From ddbe3a2e768b810f16f03ad58fd8d980647971fd Mon Sep 17 00:00:00 2001 From: nomyfan Date: Mon, 5 Apr 2021 09:41:22 +0800 Subject: [PATCH 1/3] feat: make it working on Windows * capture packets from an interface * conditional compilation * fix overflow in get_time function --- Cargo.lock | 2 +- Cargo.toml | 9 +++- src/main.rs | 118 ++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 117 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9039c43..7194bd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ dependencies = [ [[package]] name = "dnspeep" -version = "0.1.2" +version = "0.1.3" dependencies = [ "bytes 1.0.1", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 6df5e72..3fa6014 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "dnspeep" -version = "0.1.2" +version = "0.1.3" authors = ["Julia Evans "] edition = "2018" [dependencies] -pcap = { git = "https://github.com/jvns/pcap", features=["capture-stream"] } +pcap = { git = "https://github.com/jvns/pcap" } libc = "0.2.80" dns-message-parser = "~0.5" etherparse = "0.9.0" @@ -16,3 +16,8 @@ getopts = "0.2.17" bytes = "~1.0" hex = "~0.4" chrono = "0.4.19" + +[features] +default = [] +pcap_windows = [] +pcap_unix = ["pcap/capture-stream"] diff --git a/src/main.rs b/src/main.rs index 1738505..5053443 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,15 @@ use dns_message_parser::Dns; use etherparse::IpHeader; use etherparse::PacketHeaders; use eyre::{Result, WrapErr}; +#[cfg(not(windows))] use futures::StreamExt; use getopts::Options; use hex::encode; +#[cfg(not(windows))] use pcap::stream::{PacketCodec, PacketStream}; -use pcap::{Active, Capture, Linktype, Packet}; +#[cfg(not(windows))] +use pcap::Active; +use pcap::{Capture, Device, Linktype, Packet}; use std::collections::HashMap; use std::env; use std::net::IpAddr; @@ -40,6 +44,7 @@ struct Opts { enum Source { Port(u16), Filename(String), + Interface(Device), } impl Opts { @@ -81,14 +86,25 @@ async fn main() -> Result<()> { let map = Arc::new(Mutex::new(HashMap::new())); let opts = parse_args()?; opts.print_header(); + match opts.clone().source { + #[allow(unused_variables)] Source::Port(port) => { - let stream = capture_stream(opts, map.clone(), port)?; - capture_packets(stream).await; + #[cfg(not(windows))] + { + let stream = capture_stream(opts, map.clone(), port)?; + capture_packets(stream).await; + } + #[cfg(windows)] + { + eprintln!("On windows, use `interface` or `file` instead."); + std::process::exit(1); + } } Source::Filename(filename) => { capture_file(&opts, &filename)?; } + Source::Interface(interface) => capture_interface(interface, map, opts)?, }; Ok(()) } @@ -106,6 +122,14 @@ fn parse_args() -> Result { "print timestamp and elapsed time for each query", ); opts.optflag("h", "help", "print this help menu"); + opts.optopt( + "i", + "interface", + "capture packets from an interface", + "INTERFACE_NAME", + ); + opts.optflag("l", "list", "list all interfaces"); + let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { @@ -116,12 +140,37 @@ fn parse_args() -> Result { print_usage(&program, opts); std::process::exit(0); } + + if matches.opt_present("l") { + list_all_interfaces()?; + std::process::exit(0); + } + let mut opts = Opts { source: Source::Port(53), timestamp: matches.opt_present("t"), }; - if let Some(filename) = matches.opt_str("f") { + if let Some(interface_name) = matches.opt_str("i") { + let interface = pcap::Device::list()? + .iter() + .filter(|x| x.name.eq(&interface_name)) + .nth(0) + .map(|x| x.to_owned()); + + match interface { + Some(interface) => { + opts.source = Source::Interface(interface); + } + None => { + eprintln!( + "Cannot find an interface with the name `{}`", + &interface_name + ); + std::process::exit(1); + } + } + } else if let Some(filename) = matches.opt_str("f") { opts.source = Source::Filename(filename.to_string()); } else if let Some(port_str) = matches.opt_str("p") { match port_str.parse() { @@ -164,6 +213,7 @@ fn capture_file(opts: &Opts, filename: &str) -> Result<()> { Ok(()) } +#[cfg(not(windows))] fn capture_stream( opts: Opts, map: Arc>>, @@ -187,20 +237,61 @@ fn capture_stream( .wrap_err("Failed to create stream") } +#[cfg(not(windows))] async fn capture_packets(mut stream: PacketStream) { while stream.next().await.is_some() {} } +fn capture_interface( + interface: Device, + map: Arc>>, + opts: Opts, +) -> Result<()> { + let mut cap = pcap::Capture::from_device(interface) + .wrap_err("Failed to find the interface")? + .immediate_mode(true) + .open() + .wrap_err("Failed to start.")?; + + cap.filter(format!("udp and port {}", 53).as_str(), true) + .expect("Failed to create BPF filter"); + + let mut decoder = PrintCodec { + map, + linktype: cap.get_datalink(), + opts, + }; + + while let Ok(packet) = cap.next() { + decoder.decode_packet(packet)? + } + + Ok(()) +} + +fn list_all_interfaces() -> Result<()> { + let empty_str = "".to_string(); + + let interfaces = + pcap::Device::list().wrap_err("Encounter error while listing interfaces on your device")?; + println!("{:55} {}", "Interface Name", "Interface Description"); + interfaces.iter().for_each(|it| { + let name = &it.name; + let desc = it.desc.as_ref().unwrap_or(&empty_str); + println!("{:55} {}", name, desc); + }); + + Ok(()) +} + pub struct PrintCodec { map: Arc>>, linktype: Linktype, opts: Opts, } -impl PacketCodec for PrintCodec { - type Type = (); - - fn decode(&mut self, packet: Packet) -> Result<(), pcap::Error> { +impl PrintCodec { + pub fn decode_packet(&mut self, packet: Packet) -> Result<(), pcap::Error> { let mut map = self.map.lock().unwrap(); let map_clone = self.map.clone(); let opts_clone = self.opts.clone(); @@ -230,9 +321,18 @@ impl PacketCodec for PrintCodec { } } +#[cfg(not(windows))] +impl PacketCodec for PrintCodec { + type Type = (); + + fn decode(&mut self, packet: Packet) -> Result<(), pcap::Error> { + self.decode_packet(packet) + } +} + fn get_time(packet: &Packet) -> DateTime { let packet_time = packet.header.ts; - let micros = ((packet_time.tv_sec * 1000000) as u64) + (packet_time.tv_usec as u64); + let micros = (packet_time.tv_sec as u64 * 1000000) + (packet_time.tv_usec as u64); DateTime::::from(time::UNIX_EPOCH + time::Duration::from_micros(micros)) } From bb329cf84fc37a789d35e8f6485c179f348dd0ce Mon Sep 17 00:00:00 2001 From: nomyfan Date: Mon, 5 Apr 2021 10:38:31 +0800 Subject: [PATCH 2/3] ci: set cargo build features --- .github/workflows/publish.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3f8036d..8d8c2db 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,10 +19,12 @@ jobs: target: x86_64-unknown-linux-gnu os: ubuntu-latest asset_name: dnspeep-linux.tar.gz + features: pcap_unix - name: macos target: x86_64-apple-darwin os: macos-latest asset_name: dnspeep-macos.tar.gz + features: pcap_unix steps: - uses: actions/checkout@v1 @@ -58,7 +60,7 @@ jobs: rustup target add ${{ matrix.target }} - name: Build - run: cargo build --release --locked --target ${{ matrix.target }} + run: cargo build --release --locked --target ${{ matrix.target }} --features ${{ matrix.features }} - name: Create release tarball run: tar -C target/${{matrix.target}}/release/ -czf ${{ matrix.asset_name }} dnspeep From 4307ca2f1bfd853b4b3b57ed1f0d66b7093a010c Mon Sep 17 00:00:00 2001 From: nomyfan Date: Fri, 23 Apr 2021 12:11:19 +0800 Subject: [PATCH 3/3] feat: support selecting an interface and port both windows and unix are supported. on unix, default interface is `any`. default port is 53. --- src/main.rs | 120 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 31 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5053443..793eda2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,14 +40,72 @@ struct Opts { timestamp: bool, } +#[derive(Clone)] +struct Interface { + device: Device, + port: u16, +} + +fn find_device_with_name(name: &str) -> Result> { + let device = pcap::Device::list()? + .iter() + .filter(|x| x.name.eq(name)) + .nth(0) + .map(|x| x.to_owned()); + + Ok(device) +} + +impl Interface { + #[cfg(windows)] + fn from_default() -> Result { + let device = pcap::Device::lookup().wrap_err("Cannot find any interface on your device")?; + // pcap doesn't return the device's desc in `lookup` method. + // Using unwrap is safe here since we have gotten the correct name. + let device = find_device_with_name(&device.name)?.unwrap(); + + Ok(Interface::from_device(device)) + } + + #[cfg(not(windows))] + fn from_any() -> Result { + let device = find_device_with_name("any").wrap_err("Cannot find the interface `any`")?; + match device { + Some(dev) => Ok(Interface::from_device(dev)), + None => panic!("Cannot find the interface `any`"), + } + } + + fn from_device(device: Device) -> Interface { + Interface { device, port: 53 } + } +} + #[derive(Clone)] enum Source { - Port(u16), + Interface(Interface), Filename(String), - Interface(Device), } impl Opts { + fn print_source(self: &Opts) { + match &self.source { + Source::Interface(iter) => { + let name = &iter.device.name; + let desc = iter + .device + .desc + .as_ref() + .map(|desc| format!("({})", desc)) + .unwrap_or(String::from("")); + let port = iter.port; + println!("Capturing from interface {}{} at port {}", name, desc, port) + } + Source::Filename(filename) => { + println!("Capturing from file {}", filename) + } + } + } fn print_header(self: &Opts) { if self.timestamp { println!( @@ -85,26 +143,25 @@ impl Opts { async fn main() -> Result<()> { let map = Arc::new(Mutex::new(HashMap::new())); let opts = parse_args()?; + opts.print_source(); opts.print_header(); match opts.clone().source { - #[allow(unused_variables)] - Source::Port(port) => { - #[cfg(not(windows))] + Source::Interface(interface) => { + #[cfg(windows)] { - let stream = capture_stream(opts, map.clone(), port)?; - capture_packets(stream).await; + capture_interface(interface, map, opts)? } - #[cfg(windows)] + + #[cfg(not(windows))] { - eprintln!("On windows, use `interface` or `file` instead."); - std::process::exit(1); + let stream = capture_stream(opts, map, interface)?; + capture_packets(stream).await; } } Source::Filename(filename) => { capture_file(&opts, &filename)?; } - Source::Interface(interface) => capture_interface(interface, map, opts)?, }; Ok(()) } @@ -147,20 +204,17 @@ fn parse_args() -> Result { } let mut opts = Opts { - source: Source::Port(53), + #[cfg(windows)] + source: Source::Interface(Interface::from_default()?), + #[cfg(not(windows))] + source: Source::Interface(Interface::from_any()?), timestamp: matches.opt_present("t"), }; if let Some(interface_name) = matches.opt_str("i") { - let interface = pcap::Device::list()? - .iter() - .filter(|x| x.name.eq(&interface_name)) - .nth(0) - .map(|x| x.to_owned()); - - match interface { - Some(interface) => { - opts.source = Source::Interface(interface); + match find_device_with_name(&interface_name)? { + Some(device) => { + opts.source = Source::Interface(Interface::from_device(device)); } None => { eprintln!( @@ -172,11 +226,14 @@ fn parse_args() -> Result { } } else if let Some(filename) = matches.opt_str("f") { opts.source = Source::Filename(filename.to_string()); - } else if let Some(port_str) = matches.opt_str("p") { + } + if let Some(port_str) = matches.opt_str("p") { match port_str.parse() { - Ok(port) => { - opts.source = Source::Port(port); - } + // set port to current interface config + Ok(port) => match &mut opts.source { + Source::Interface(iter) => iter.port = port, + _ => {} + }, Err(_) => { eprintln!("Invalid port number: {}", &port_str); std::process::exit(1); @@ -217,9 +274,9 @@ fn capture_file(opts: &Opts, filename: &str) -> Result<()> { fn capture_stream( opts: Opts, map: Arc>>, - port: u16, + interface: Interface, ) -> Result> { - let mut cap = Capture::from_device("any") + let mut cap = Capture::from_device(interface.device) .wrap_err("Failed to find device 'any'")? .immediate_mode(true) .open() @@ -227,7 +284,7 @@ fn capture_stream( .setnonblock() .wrap_err("Failed to set nonblocking")?; let linktype = cap.get_datalink(); - cap.filter(format!("udp and port {}", port).as_str(), true) + cap.filter(format!("udp and port {}", interface.port).as_str(), true) .wrap_err("Failed to create BPF filter")?; cap.stream(PrintCodec { map, @@ -242,18 +299,19 @@ async fn capture_packets(mut stream: PacketStream) { while stream.next().await.is_some() {} } +#[cfg(windows)] fn capture_interface( - interface: Device, + interface: Interface, map: Arc>>, opts: Opts, ) -> Result<()> { - let mut cap = pcap::Capture::from_device(interface) + let mut cap = pcap::Capture::from_device(interface.device) .wrap_err("Failed to find the interface")? .immediate_mode(true) .open() .wrap_err("Failed to start.")?; - cap.filter(format!("udp and port {}", 53).as_str(), true) + cap.filter(format!("udp and port {}", interface.port).as_str(), true) .expect("Failed to create BPF filter"); let mut decoder = PrintCodec {