diff --git a/.gitignore b/.gitignore index 911ee60..0333ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.idea .envrc *.log +*.pcap diff --git a/README.md b/README.md index 02754d4..fbda40f 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,17 @@ INFO onetun::tunnel > Tunneling TCP [[::1]:8080]->[192.168.4.2:8080] (via [140. INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) ``` +### Packet Capture + +For debugging purposes, you can enable the capture of IP packets sent between onetun and the WireGuard peer. +The output is a libpcap capture file that can be viewed with Wireshark. + +``` +$ ./onetun --pcap wg.pcap 127.0.0.1:8080:192.168.4.2:8080 +INFO onetun::pcap > Capturing WireGuard IP packets to wg.pcap +INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +``` + ## Download Normally I would publish `onetun` to crates.io. However, it depends on some features diff --git a/src/config.rs b/src/config.rs index 8df9511..e4ea700 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,6 +20,7 @@ pub struct Config { pub(crate) max_transmission_unit: usize, pub(crate) log: String, pub(crate) warnings: Vec, + pub(crate) pcap_file: Option, } impl Config { @@ -96,7 +97,13 @@ impl Config { .long("log") .env("ONETUN_LOG") .default_value("info") - .help("Configures the log level and format.") + .help("Configures the log level and format."), + Arg::with_name("pcap") + .required(false) + .takes_value(true) + .long("pcap") + .env("ONETUN_PCAP") + .help("Decrypts and captures IP packets on the WireGuard tunnel to a given output file.") ]).get_matches(); // Combine `PORT_FORWARD` arg and `ONETUN_PORT_FORWARD_#` envs @@ -174,6 +181,7 @@ impl Config { max_transmission_unit: parse_mtu(matches.value_of("max-transmission-unit")) .with_context(|| "Invalid max-transmission-unit value")?, log: matches.value_of("log").unwrap_or_default().into(), + pcap_file: matches.value_of("pcap").map(String::from), warnings, }) } diff --git a/src/main.rs b/src/main.rs index 2377245..879b45e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ use crate::wg::WireGuardTunnel; pub mod config; pub mod events; +pub mod pcap; pub mod tunnel; pub mod virtual_device; pub mod virtual_iface; @@ -37,6 +38,12 @@ async fn main() -> anyhow::Result<()> { let bus = Bus::default(); + if let Some(pcap_file) = config.pcap_file.clone() { + // Start packet capture + let bus = bus.clone(); + tokio::spawn(async move { pcap::capture(pcap_file, bus).await }); + } + let wg = WireGuardTunnel::new(&config, bus.clone()) .await .with_context(|| "Failed to initialize WireGuard tunnel")?; diff --git a/src/pcap.rs b/src/pcap.rs new file mode 100644 index 0000000..1771a33 --- /dev/null +++ b/src/pcap.rs @@ -0,0 +1,113 @@ +use crate::events::Event; +use crate::Bus; +use anyhow::Context; +use smoltcp::time::Instant; +use tokio::fs::File; +use tokio::io::{AsyncWriteExt, BufWriter}; + +struct Pcap { + writer: BufWriter, +} + +/// libpcap file writer +/// This is mostly taken from `smoltcp`, but rewritten to be async. +impl Pcap { + async fn flush(&mut self) -> anyhow::Result<()> { + self.writer + .flush() + .await + .with_context(|| "Failed to flush pcap writer") + } + + async fn write(&mut self, data: &[u8]) -> anyhow::Result { + self.writer + .write(data) + .await + .with_context(|| format!("Failed to write {} bytes to pcap writer", data.len())) + } + + async fn write_u16(&mut self, value: u16) -> anyhow::Result<()> { + self.writer + .write_u16(value) + .await + .with_context(|| "Failed to write u16 to pcap writer") + } + + async fn write_u32(&mut self, value: u32) -> anyhow::Result<()> { + self.writer + .write_u32(value) + .await + .with_context(|| "Failed to write u32 to pcap writer") + } + + async fn global_header(&mut self) -> anyhow::Result<()> { + self.write_u32(0xa1b2c3d4).await?; // magic number + self.write_u16(2).await?; // major version + self.write_u16(4).await?; // minor version + self.write_u32(0).await?; // timezone (= UTC) + self.write_u32(0).await?; // accuracy (not used) + self.write_u32(65535).await?; // maximum packet length + self.write_u32(101).await?; // link-layer header type (101 = IP) + self.flush().await + } + + async fn packet_header(&mut self, timestamp: Instant, length: usize) -> anyhow::Result<()> { + assert!(length <= 65535); + + self.write_u32(timestamp.secs() as u32).await?; // timestamp seconds + self.write_u32(timestamp.micros() as u32).await?; // timestamp microseconds + self.write_u32(length as u32).await?; // captured length + self.write_u32(length as u32).await?; // original length + Ok(()) + } + + async fn packet(&mut self, timestamp: Instant, packet: &[u8]) -> anyhow::Result<()> { + self.packet_header(timestamp, packet.len()) + .await + .with_context(|| "Failed to write packet header to pcap writer")?; + self.write(packet) + .await + .with_context(|| "Failed to write packet to pcap writer")?; + self.writer + .flush() + .await + .with_context(|| "Failed to flush pcap writer")?; + self.flush().await + } +} + +/// Listens on the event bus for IP packets sent from and to the WireGuard tunnel. +pub async fn capture(pcap_file: String, bus: Bus) -> anyhow::Result<()> { + let mut endpoint = bus.new_endpoint(); + let file = File::create(&pcap_file) + .await + .with_context(|| "Failed to create pcap file")?; + let writer = BufWriter::new(file); + + let mut writer = Pcap { writer }; + writer + .global_header() + .await + .with_context(|| "Failed to write global header to pcap writer")?; + + info!("Capturing WireGuard IP packets to {}", &pcap_file); + loop { + match endpoint.recv().await { + Event::InboundInternetPacket(_proto, ip) => { + let instant = Instant::now(); + writer + .packet(instant, &ip) + .await + .with_context(|| "Failed to write inbound IP packet to pcap writer")?; + } + Event::OutboundInternetPacket(ip) => { + let instant = Instant::now(); + writer + .packet(instant, &ip) + .await + .with_context(|| "Failed to write output IP packet to pcap writer")?; + } + _ => {} + } + } +}