Add optional IP packet capture for WireGuard tunnel

This commit is contained in:
Aram 🍐 2022-01-08 16:05:14 -05:00
parent 953bc18279
commit ff0f5b967e
5 changed files with 141 additions and 1 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/.idea /.idea
.envrc .envrc
*.log *.log
*.pcap

View file

@ -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) 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 ## Download
Normally I would publish `onetun` to crates.io. However, it depends on some features Normally I would publish `onetun` to crates.io. However, it depends on some features

View file

@ -20,6 +20,7 @@ pub struct Config {
pub(crate) max_transmission_unit: usize, pub(crate) max_transmission_unit: usize,
pub(crate) log: String, pub(crate) log: String,
pub(crate) warnings: Vec<String>, pub(crate) warnings: Vec<String>,
pub(crate) pcap_file: Option<String>,
} }
impl Config { impl Config {
@ -96,7 +97,13 @@ impl Config {
.long("log") .long("log")
.env("ONETUN_LOG") .env("ONETUN_LOG")
.default_value("info") .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(); ]).get_matches();
// Combine `PORT_FORWARD` arg and `ONETUN_PORT_FORWARD_#` envs // 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")) max_transmission_unit: parse_mtu(matches.value_of("max-transmission-unit"))
.with_context(|| "Invalid max-transmission-unit value")?, .with_context(|| "Invalid max-transmission-unit value")?,
log: matches.value_of("log").unwrap_or_default().into(), log: matches.value_of("log").unwrap_or_default().into(),
pcap_file: matches.value_of("pcap").map(String::from),
warnings, warnings,
}) })
} }

View file

@ -17,6 +17,7 @@ use crate::wg::WireGuardTunnel;
pub mod config; pub mod config;
pub mod events; pub mod events;
pub mod pcap;
pub mod tunnel; pub mod tunnel;
pub mod virtual_device; pub mod virtual_device;
pub mod virtual_iface; pub mod virtual_iface;
@ -37,6 +38,12 @@ async fn main() -> anyhow::Result<()> {
let bus = Bus::default(); 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()) let wg = WireGuardTunnel::new(&config, bus.clone())
.await .await
.with_context(|| "Failed to initialize WireGuard tunnel")?; .with_context(|| "Failed to initialize WireGuard tunnel")?;

113
src/pcap.rs Normal file
View file

@ -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<File>,
}
/// 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<usize> {
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")?;
}
_ => {}
}
}
}