summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2025-07-28 17:36:44 +0200
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2025-10-03 12:39:27 +0200
commit84adeff45f4b2b6092e3b5a22123234248320bb6 (patch)
tree06c1ec2323e5e75e0ababd6fd947caa9780414fb
parent41142a60af334dba61f57ec74e5e5bd2577a4266 (diff)
downloadmullvadvpn-84adeff45f4b2b6092e3b5a22123234248320bb6.tar.xz
mullvadvpn-84adeff45f4b2b6092e3b5a22123234248320bb6.zip
Add a UDS for wiresharking gotatun multihop traffic
-rw-r--r--Cargo.lock32
-rw-r--r--mullvad-daemon/Cargo.toml1
-rw-r--r--talpid-core/Cargo.toml1
-rw-r--r--talpid-wireguard/Cargo.toml1
-rw-r--r--talpid-wireguard/src/boringtun/mod.rs76
5 files changed, 110 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6bc38cb52e..ba67067ff2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -460,6 +460,7 @@ dependencies = [
"log",
"nix 0.30.1",
"parking_lot",
+ "pcap-file",
"pnet_packet 0.35.0",
"rand 0.9.2",
"rand_core 0.6.4",
@@ -494,6 +495,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
+name = "byteorder_slice"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b294e30387378958e8bf8f4242131b930ea615ff81e8cac2440cea0a6013190"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1107,6 +1117,17 @@ dependencies = [
]
[[package]]
+name = "derive-into-owned"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d94d81e3819a7b06a8638f448bc6339371ca9b6076a99d4a43eece3c4c923"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
name = "derive-try-from-primitive"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3983,6 +4004,17 @@ dependencies = [
]
[[package]]
+name = "pcap-file"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc1f139757b058f9f37b76c48501799d12c9aa0aa4c0d4c980b062ee925d1b2"
+dependencies = [
+ "byteorder_slice",
+ "derive-into-owned",
+ "thiserror 1.0.59",
+]
+
+[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml
index e98b126942..dfc1607467 100644
--- a/mullvad-daemon/Cargo.toml
+++ b/mullvad-daemon/Cargo.toml
@@ -15,6 +15,7 @@ workspace = true
api-override = ["mullvad-api/api-override"]
boringtun = ["talpid-core/boringtun"]
staggered-obfuscation = ["mullvad-relay-selector/staggered-obfuscation"]
+multihop-pcap = ["talpid-core/multihop-pcap"]
[dependencies]
anyhow = { workspace = true }
diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml
index f2919169da..0c14671c6e 100644
--- a/talpid-core/Cargo.toml
+++ b/talpid-core/Cargo.toml
@@ -12,6 +12,7 @@ workspace = true
[features]
boringtun = ["talpid-wireguard/boringtun"]
+multihop-pcap = ["talpid-wireguard/multihop-pcap"]
[dependencies]
anyhow = { workspace = true }
diff --git a/talpid-wireguard/Cargo.toml b/talpid-wireguard/Cargo.toml
index fb198c9502..306c58e845 100644
--- a/talpid-wireguard/Cargo.toml
+++ b/talpid-wireguard/Cargo.toml
@@ -12,6 +12,7 @@ workspace = true
[features]
boringtun = ["dep:boringtun", "dep:tun07", "talpid-tunnel/boringtun"]
+multihop-pcap = ["boringtun/pcap"]
[dependencies]
async-trait = "0.1"
diff --git a/talpid-wireguard/src/boringtun/mod.rs b/talpid-wireguard/src/boringtun/mod.rs
index c854213d71..d1ad0b1587 100644
--- a/talpid-wireguard/src/boringtun/mod.rs
+++ b/talpid-wireguard/src/boringtun/mod.rs
@@ -31,6 +31,12 @@ use talpid_tunnel::tun_provider::{self, Tun, TunProvider};
use talpid_tunnel_config_client::DaitaSettings;
use tun07::{AbstractDevice, AsyncDevice};
+#[cfg(all(feature = "multihop-pcap", target_os = "linux"))]
+use boringtun::tun::{
+ IpRecv, IpSend,
+ pcap::{PcapSniffer, PcapStream},
+};
+
#[cfg(target_os = "android")]
type UdpFactory = AndroidUdpSocketFactory;
@@ -38,9 +44,17 @@ type UdpFactory = AndroidUdpSocketFactory;
type UdpFactory = UdpSocketFactory;
type SinglehopDevice = DeviceHandle<(UdpFactory, Arc<tun07::AsyncDevice>, Arc<tun07::AsyncDevice>)>;
-type EntryDevice = DeviceHandle<(UdpFactory, TunChannelTx, TunChannelRx)>;
type ExitDevice = DeviceHandle<(PacketChannelUdp, Arc<AsyncDevice>, Arc<AsyncDevice>)>;
+#[cfg(not(all(feature = "multihop-pcap", target_os = "linux")))]
+type EntryDevice = DeviceHandle<(UdpFactory, TunChannelTx, TunChannelRx)>;
+#[cfg(all(feature = "multihop-pcap", target_os = "linux"))]
+type EntryDevice = DeviceHandle<(
+ UdpFactory,
+ PcapSniffer<TunChannelTx>,
+ PcapSniffer<TunChannelRx>,
+)>;
+
const PACKET_CHANNEL_CAPACITY: usize = 100;
pub struct BoringTun {
@@ -261,6 +275,11 @@ async fn create_devices(
#[cfg(not(target_os = "android"))]
let factory = UdpSocketFactory;
+ // Hacky way of dumping entry<->exit traffic to a unix socket which wireshark can read.
+ // See docs on wrap_in_pcap_sniffer for an explanation.
+ #[cfg(all(feature = "multihop-pcap", target_os = "linux"))]
+ let (tun_tx, tun_rx) = wrap_in_pcap_sniffer(tun_tx, tun_rx);
+
let entry_device = EntryDevice::new(factory, tun_tx, tun_rx, boringtun_entry_config).await;
let private_key = &config.tunnel.private_key;
@@ -570,3 +589,58 @@ pub fn get_tunnel_for_userspace(
last_error.expect("Should be collected in loop"),
))
}
+
+/// Wrap `ip_send` and `ip_recv` in [PcapSniffer]s for use with Wireshark.
+///
+/// With userspace multihop, the [ExitDevice] communicates with the network through the
+/// [EntryDevice], without going through the kernel. That means there is no network interface
+/// for wireshark to sniff. By interposing [PcapSniffer]s, any packets that are sent to `ip_send`,
+/// or received from `ip_recv`, will _also_ be written to a unix socket, encoded using the pcap
+/// file format.
+///
+/// The unix socket can be opened in wireshark to inspect communication with the [ExitDevice]s peer.
+/// ```sh
+/// wireshark -k -i /tmp/mullvad-multihop.pcap
+/// ```
+#[cfg(all(feature = "multihop-pcap", target_os = "linux"))]
+fn wrap_in_pcap_sniffer<S, R>(ip_send: S, ip_recv: R) -> (PcapSniffer<S>, PcapSniffer<R>)
+where
+ S: IpSend,
+ R: IpRecv,
+{
+ use std::{
+ fs,
+ os::unix::{fs::PermissionsExt, net::UnixListener},
+ sync::LazyLock,
+ time::Instant,
+ };
+
+ const SOCKET_PATH: &str = "/tmp/mullvad-multihop.pcap";
+
+ /// The global pcap writer. We initialize it once so that we can re-use the same unix socket
+ /// for the entire lifetime of the application.
+ static WRITER: LazyLock<PcapStream> = LazyLock::new(|| {
+ log::warn!("Binding pcap socket to {SOCKET_PATH:?}");
+ let _ = fs::remove_file(SOCKET_PATH);
+ let listener = UnixListener::bind(SOCKET_PATH).unwrap();
+ let _ = fs::set_permissions(SOCKET_PATH, fs::Permissions::from_mode(0o777));
+
+ log::warn!("Waiting for connection to pcap socket");
+ log::warn!(" wireshark -k -i {SOCKET_PATH:?}");
+ let (stream, _) = listener
+ .accept()
+ .expect("Error while waiting for pcap listener");
+
+ PcapStream::new(Box::new(stream))
+ });
+
+ let start_time = Instant::now();
+
+ let w = WRITER.clone();
+ let ip_send = PcapSniffer::new(ip_send, w, start_time);
+
+ let w = WRITER.clone();
+ let ip_recv = PcapSniffer::new(ip_recv, w, start_time);
+
+ (ip_send, ip_recv)
+}