diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-09-05 13:02:33 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-09-15 09:21:28 +0200 |
| commit | 344b9291ba15b6609d79798d7e21997f97fbc4d9 (patch) | |
| tree | 42bda66a2f74afda53cb7a73ab87f8948de4b72a | |
| parent | 29ba9088210475eb179c8eefe5c9f3b8bbc92583 (diff) | |
| download | mullvadvpn-344b9291ba15b6609d79798d7e21997f97fbc4d9.tar.xz mullvadvpn-344b9291ba15b6609d79798d7e21997f97fbc4d9.zip | |
Add LWO obfuscator
21 files changed, 575 insertions, 90 deletions
diff --git a/Cargo.lock b/Cargo.lock index eb876b6dfb..fed1ebf2a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -902,6 +902,7 @@ dependencies = [ "serde", "serde_json", "tinytemplate", + "tokio", "walkdir", ] @@ -4532,7 +4533,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.100", @@ -6441,11 +6442,14 @@ name = "tunnel-obfuscation" version = "0.0.0" dependencies = [ "async-trait", + "criterion", "log", "mullvad-masque-proxy", "nix 0.30.1", + "rand 0.8.5", "shadowsocks", "socket2 0.5.8", + "talpid-types", "thiserror 2.0.9", "tokio", "tokio-util 0.7.10", diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt index 9b4dd07056..7889d1ea2d 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt @@ -223,6 +223,8 @@ internal fun ManagementInterface.ObfuscationEndpoint.ObfuscationType.toDomain(): ManagementInterface.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS -> ObfuscationType.Shadowsocks ManagementInterface.ObfuscationEndpoint.ObfuscationType.QUIC -> ObfuscationType.Quic + ManagementInterface.ObfuscationEndpoint.ObfuscationType.LWO -> + throw IllegalArgumentException("Unsupported obfuscation type") ManagementInterface.ObfuscationEndpoint.ObfuscationType.UNRECOGNIZED -> throw IllegalArgumentException("Unrecognized obfuscation type") } @@ -429,6 +431,8 @@ internal fun ManagementInterface.ObfuscationSettings.SelectedObfuscation.toDomai ManagementInterface.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS -> ObfuscationMode.Shadowsocks ManagementInterface.ObfuscationSettings.SelectedObfuscation.QUIC -> ObfuscationMode.Quic + ManagementInterface.ObfuscationSettings.SelectedObfuscation.LWO -> + throw IllegalArgumentException("Unsupported obfuscation type") ManagementInterface.ObfuscationSettings.SelectedObfuscation.UNRECOGNIZED -> throw IllegalArgumentException("Unrecognized selected obfuscation") } diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index e9e13874ee..46701136a9 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -337,6 +337,7 @@ message ObfuscationEndpoint { UDP2TCP = 0; SHADOWSOCKS = 1; QUIC = 2; + LWO = 3; } string address = 1; @@ -432,6 +433,7 @@ message ObfuscationSettings { UDP2TCP = 2; SHADOWSOCKS = 3; QUIC = 4; + LWO = 5; } SelectedObfuscation selected_obfuscation = 1; Udp2TcpObfuscationSettings udp2tcp = 2; diff --git a/mullvad-management-interface/src/types/conversions/net.rs b/mullvad-management-interface/src/types/conversions/net.rs index 9defbd3d85..478400346f 100644 --- a/mullvad-management-interface/src/types/conversions/net.rs +++ b/mullvad-management-interface/src/types/conversions/net.rs @@ -42,6 +42,9 @@ impl From<talpid_types::net::TunnelEndpoint> for proto::TunnelEndpoint { net::ObfuscationType::Quic => { i32::from(proto::obfuscation_endpoint::ObfuscationType::Quic) } + net::ObfuscationType::Lwo => { + i32::from(proto::obfuscation_endpoint::ObfuscationType::Lwo) + } }, } }), @@ -129,6 +132,9 @@ impl TryFrom<proto::TunnelEndpoint> for talpid_types::net::TunnelEndpoint { Ok(proto::obfuscation_endpoint::ObfuscationType::Quic) => { talpid_net::ObfuscationType::Quic } + Ok(proto::obfuscation_endpoint::ObfuscationType::Lwo) => { + talpid_net::ObfuscationType::Lwo + } Err(_) => { return Err(FromProtobufTypeError::InvalidArgument( "unknown obfuscation type", diff --git a/mullvad-management-interface/src/types/conversions/relay_constraints.rs b/mullvad-management-interface/src/types/conversions/relay_constraints.rs index 4e57153c51..2a513b58fe 100644 --- a/mullvad-management-interface/src/types/conversions/relay_constraints.rs +++ b/mullvad-management-interface/src/types/conversions/relay_constraints.rs @@ -164,6 +164,7 @@ impl From<&mullvad_types::relay_constraints::ObfuscationSettings> for proto::Obf proto::obfuscation_settings::SelectedObfuscation::Shadowsocks } SelectedObfuscation::Quic => proto::obfuscation_settings::SelectedObfuscation::Quic, + SelectedObfuscation::Lwo => proto::obfuscation_settings::SelectedObfuscation::Lwo, }); Self { selected_obfuscation, @@ -459,6 +460,7 @@ impl TryFrom<proto::ObfuscationSettings> for mullvad_types::relay_constraints::O Ok(IpcSelectedObfuscation::Udp2tcp) => SelectedObfuscation::Udp2Tcp, Ok(IpcSelectedObfuscation::Shadowsocks) => SelectedObfuscation::Shadowsocks, Ok(IpcSelectedObfuscation::Quic) => SelectedObfuscation::Quic, + Ok(IpcSelectedObfuscation::Lwo) => SelectedObfuscation::Lwo, Err(_) => { return Err(FromProtobufTypeError::InvalidArgument( "invalid obfuscation settings", diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs index 1a5a11e69c..7e9ae866fc 100644 --- a/mullvad-relay-selector/src/relay_selector/helpers.rs +++ b/mullvad-relay-selector/src/relay_selector/helpers.rs @@ -162,6 +162,27 @@ pub fn get_quic_obfuscator(relay: Relay, ip_version: IpVersion) -> Option<Select Some(obfuscator) } +pub fn get_lwo_obfuscator( + relay: Relay, + endpoint: &MullvadWireguardEndpoint, +) -> Option<SelectedObfuscator> { + let _wg = relay.wireguard()?; + + // TODO: check if LWO is supported on this relay + + let ip = match endpoint.peer.endpoint { + SocketAddr::V4(_) => IpAddr::V4(relay.ipv4_addr_in), + SocketAddr::V6(_) => IpAddr::V6(relay.ipv6_addr_in?), + }; + let port = endpoint.peer.endpoint.port(); + let endpoint = SocketAddr::new(ip, port); + + let config = ObfuscatorConfig::Lwo { endpoint }; + + let obfuscator = SelectedObfuscator { config, relay }; + Some(obfuscator) +} + /// Return an obfuscation config for the wireguard server at `wg_in_addr` or one of `extra_in_addrs` /// (unless empty). `wg_in_addr_port_ranges` contains all valid ports for `wg_in_addr`, and /// `SHADOWSOCKS_EXTRA_PORT_RANGES` contains valid ports for `extra_in_addrs`. diff --git a/mullvad-relay-selector/src/relay_selector/matcher.rs b/mullvad-relay-selector/src/relay_selector/matcher.rs index 80af27e9c1..14402feb67 100644 --- a/mullvad-relay-selector/src/relay_selector/matcher.rs +++ b/mullvad-relay-selector/src/relay_selector/matcher.rs @@ -145,6 +145,8 @@ fn filter_on_obfuscation( }, None => false, }, + // TODO: This is only enabled on some relays + ObfuscationQuery::Lwo => true, // Other relays are compatible with this query ObfuscationQuery::Off | ObfuscationQuery::Auto | ObfuscationQuery::Udp2tcp(_) => true, } diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index da49aa0739..1105394c1b 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -948,6 +948,7 @@ impl RelaySelector { let ip_version = resolve_ip_version(query.wireguard_constraints().ip_version); Ok(helpers::get_quic_obfuscator(obfuscator_relay, ip_version)) } + ObfuscationQuery::Lwo => Ok(helpers::get_lwo_obfuscator(obfuscator_relay, endpoint)), } } diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs index af4b18e045..a3fe4a24f9 100644 --- a/mullvad-relay-selector/src/relay_selector/query.rs +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -290,6 +290,7 @@ pub enum ObfuscationQuery { Udp2tcp(Udp2TcpObfuscationSettings), Shadowsocks(ShadowsocksSettings), Quic, + Lwo, } impl ObfuscationQuery { @@ -317,6 +318,10 @@ impl ObfuscationQuery { selected_obfuscation: SelectedObfuscation::Quic, ..Default::default() }, + ObfuscationQuery::Lwo => ObfuscationSettings { + selected_obfuscation: SelectedObfuscation::Lwo, + ..Default::default() + }, } } } @@ -335,6 +340,7 @@ impl From<ObfuscationSettings> for ObfuscationQuery { ObfuscationQuery::Shadowsocks(obfuscation.shadowsocks) } SelectedObfuscation::Quic => ObfuscationQuery::Quic, + SelectedObfuscation::Lwo => ObfuscationQuery::Lwo, } } } diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index 44ac76d2a5..2237289574 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -433,7 +433,8 @@ fn test_wireguard_retry_order() { ObfuscationQuery::Off => obfuscator.is_none(), ObfuscationQuery::Quic | ObfuscationQuery::Udp2tcp(_) - | ObfuscationQuery::Shadowsocks(_) => obfuscator.is_some(), + | ObfuscationQuery::Shadowsocks(_) + | ObfuscationQuery::Lwo => obfuscator.is_some(), }); } _ => unreachable!(), diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index 3dfe661819..013153524e 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -640,6 +640,7 @@ pub enum SelectedObfuscation { Udp2Tcp, Shadowsocks, Quic, + Lwo, } impl Intersection for SelectedObfuscation { @@ -665,6 +666,7 @@ impl fmt::Display for SelectedObfuscation { SelectedObfuscation::Udp2Tcp => "udp2tcp".fmt(f), SelectedObfuscation::Shadowsocks => "shadowsocks".fmt(f), SelectedObfuscation::Quic => "quic".fmt(f), + SelectedObfuscation::Lwo => "lwo".fmt(f), } } } diff --git a/talpid-types/src/net/mod.rs b/talpid-types/src/net/mod.rs index e7b73bef76..5939bfebad 100644 --- a/talpid-types/src/net/mod.rs +++ b/talpid-types/src/net/mod.rs @@ -209,6 +209,7 @@ pub enum ObfuscationType { Udp2Tcp, Shadowsocks, Quic, + Lwo, } impl fmt::Display for ObfuscationType { @@ -217,6 +218,7 @@ impl fmt::Display for ObfuscationType { ObfuscationType::Udp2Tcp => "Udp2Tcp".fmt(f), ObfuscationType::Shadowsocks => "Shadowsocks".fmt(f), ObfuscationType::Quic => "QUIC".fmt(f), + ObfuscationType::Lwo => "LWO".fmt(f), } } } @@ -235,6 +237,7 @@ impl From<&ObfuscatorConfig> for ObfuscationEndpoint { ObfuscatorConfig::Udp2Tcp { .. } => ObfuscationType::Udp2Tcp, ObfuscatorConfig::Shadowsocks { .. } => ObfuscationType::Shadowsocks, ObfuscatorConfig::Quic { .. } => ObfuscationType::Quic, + ObfuscatorConfig::Lwo { .. } => ObfuscationType::Lwo, }; ObfuscationEndpoint { diff --git a/talpid-types/src/net/obfuscation.rs b/talpid-types/src/net/obfuscation.rs index 161fac92a0..7df09ecaf4 100644 --- a/talpid-types/src/net/obfuscation.rs +++ b/talpid-types/src/net/obfuscation.rs @@ -16,6 +16,9 @@ pub enum ObfuscatorConfig { endpoint: SocketAddr, auth_token: String, }, + Lwo { + endpoint: SocketAddr, + }, } impl ObfuscatorConfig { @@ -33,6 +36,10 @@ impl ObfuscatorConfig { address: *endpoint, protocol: TransportProtocol::Udp, }, + ObfuscatorConfig::Lwo { endpoint, .. } => Endpoint { + address: *endpoint, + protocol: TransportProtocol::Udp, + }, } } } diff --git a/talpid-wireguard/src/obfuscation.rs b/talpid-wireguard/src/obfuscation.rs index 3fd4d411ea..c838ec98af 100644 --- a/talpid-wireguard/src/obfuscation.rs +++ b/talpid-wireguard/src/obfuscation.rs @@ -13,7 +13,7 @@ use talpid_tunnel::tun_provider::TunProvider; use talpid_types::{ErrorExt, net::obfuscation::ObfuscatorConfig}; use tunnel_obfuscation::{ - Settings as ObfuscationSettings, create_obfuscator, quic, shadowsocks, udp2tcp, + Settings as ObfuscationSettings, create_obfuscator, lwo, quic, shadowsocks, udp2tcp, }; /// Begin running obfuscation machine, if configured. This function will patch `config`'s endpoint @@ -33,6 +33,7 @@ pub async fn apply_obfuscation_config( }; let settings = settings_from_config( + config, obfuscator_config, obfuscation_mtu, #[cfg(target_os = "linux")] @@ -81,11 +82,12 @@ fn patch_endpoint(config: &mut Config, endpoint: SocketAddr) { } fn settings_from_config( - config: &ObfuscatorConfig, + config: &Config, + obfuscation_config: &ObfuscatorConfig, mtu: u16, #[cfg(target_os = "linux")] fwmark: Option<u32>, ) -> ObfuscationSettings { - match config { + match obfuscation_config { ObfuscatorConfig::Udp2Tcp { endpoint } => ObfuscationSettings::Udp2Tcp(udp2tcp::Settings { peer: *endpoint, #[cfg(target_os = "linux")] @@ -122,6 +124,13 @@ fn settings_from_config( } ObfuscationSettings::Quic(settings) } + ObfuscatorConfig::Lwo { endpoint } => ObfuscationSettings::Lwo(lwo::Settings { + server_addr: *endpoint, + client_public_key: config.tunnel.private_key.public_key(), + server_public_key: config.entry_peer.public_key.clone(), + #[cfg(target_os = "linux")] + fwmark, + }), } } diff --git a/tunnel-obfuscation/Cargo.toml b/tunnel-obfuscation/Cargo.toml index 47d34c10e8..36ba26e344 100644 --- a/tunnel-obfuscation/Cargo.toml +++ b/tunnel-obfuscation/Cargo.toml @@ -19,7 +19,16 @@ tokio-util = { workspace = true } udp-over-tcp = { git = "https://github.com/mullvad/udp-over-tcp", rev = "87936ac29b68b902565955f138ab02294bcc8593" } shadowsocks = { workspace = true } mullvad-masque-proxy = { path = "../mullvad-masque-proxy" } +talpid-types = { path = "../talpid-types" } +rand = { version = "0.8.5", features = ["small_rng"] } socket2 = { workspace = true, features = ["all"] } [target.'cfg(target_os="linux")'.dependencies] -nix = { workspace = true, features = ["socket"] } +nix = { workspace = true, features = ["socket"]} + +[dev-dependencies] +criterion = { version = "0.7.0", features = ["html_reports", "async_tokio"] } + +[[bench]] +name = "lwo" +harness = false diff --git a/tunnel-obfuscation/benches/lwo.rs b/tunnel-obfuscation/benches/lwo.rs new file mode 100644 index 0000000000..0a39311769 --- /dev/null +++ b/tunnel-obfuscation/benches/lwo.rs @@ -0,0 +1,61 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use rand::RngCore; +use talpid_types::net::wireguard::PublicKey; +use tunnel_obfuscation::lwo::new_rng; + +fn obfuscate(c: &mut Criterion) { + let pubkey = PublicKey::from_base64("8Ka2l4T0tVrSR5pkcsvRG++mBlxfuf8XOxpqBkOCikU=").unwrap(); + let mut rng = new_rng(); + + let mut group = c.benchmark_group("lwo"); + group.throughput(criterion::Throughput::Bytes(fake_packet().len() as u64)); + group.bench_function(BenchmarkId::new("obfuscate", fake_packet().len()), |b| { + b.iter_batched( + fake_packet, + |mut packet| { + tunnel_obfuscation::lwo::obfuscate(&mut rng, &mut packet, pubkey.as_bytes()) + }, + criterion::BatchSize::LargeInput, + ); + }); + group.finish(); +} + +fn deobfuscate(c: &mut Criterion) { + let pubkey = PublicKey::from_base64("8Ka2l4T0tVrSR5pkcsvRG++mBlxfuf8XOxpqBkOCikU=").unwrap(); + let mut rng = new_rng(); + + let mut group = c.benchmark_group("lwo"); + group.throughput(criterion::Throughput::Bytes( + obfuscated_fake_packet(&mut rng, pubkey.as_bytes()).len() as u64, + )); + group.bench_function(BenchmarkId::new("deobfuscate", fake_packet().len()), |b| { + b.iter_batched( + || obfuscated_fake_packet(&mut rng, pubkey.as_bytes()), + |mut packet| tunnel_obfuscation::lwo::deobfuscate(&mut packet, pubkey.as_bytes()), + criterion::BatchSize::LargeInput, + ); + }); + group.finish(); +} + +type MessageType = u8; + +const DATA: MessageType = 4; +const DATA_OVERHEAD_SZ: usize = 32; + +fn fake_packet() -> Vec<u8> { + let mut packet = vec![0u8; DATA_OVERHEAD_SZ + 1200]; + packet[0] = DATA; + rand::thread_rng().fill_bytes(&mut packet[DATA_OVERHEAD_SZ..]); + packet +} + +fn obfuscated_fake_packet(rng: &mut impl RngCore, key: &[u8; 32]) -> Vec<u8> { + let mut packet = fake_packet(); + tunnel_obfuscation::lwo::obfuscate(rng, &mut packet, key); + packet +} + +criterion_group!(benches, obfuscate, deobfuscate); +criterion_main!(benches); diff --git a/tunnel-obfuscation/src/lib.rs b/tunnel-obfuscation/src/lib.rs index 30a19642b3..814bd62333 100644 --- a/tunnel-obfuscation/src/lib.rs +++ b/tunnel-obfuscation/src/lib.rs @@ -1,8 +1,11 @@ use async_trait::async_trait; use std::net::SocketAddr; +use tokio::io; +pub mod lwo; pub mod quic; pub mod shadowsocks; +pub mod socket; pub mod udp2tcp; pub type Result<T> = std::result::Result<T, Error>; @@ -26,6 +29,19 @@ pub enum Error { #[error("Failed to run Quic")] RunQuicObfuscator(#[source] quic::Error), + + #[error("Failed to initialize LWO")] + CreateLwoObfuscator(#[source] lwo::Error), + + #[error("Failed to run LWO")] + RunLwoObfuscator(#[source] lwo::Error), + + #[error("Failed to bind socket")] + BindRemoteUdp(#[source] io::Error), + + #[cfg(target_os = "linux")] + #[error("Failed to set fwmark on remote socket")] + SetFwmark(#[source] nix::Error), } #[async_trait] @@ -50,6 +66,7 @@ pub enum Settings { Udp2Tcp(udp2tcp::Settings), Shadowsocks(shadowsocks::Settings), Quic(quic::Settings), + Lwo(lwo::Settings), } pub async fn create_obfuscator(settings: &Settings) -> Result<Box<dyn Obfuscator>> { @@ -58,14 +75,9 @@ pub async fn create_obfuscator(settings: &Settings) -> Result<Box<dyn Obfuscator .await .map(box_obfuscator) .map_err(Error::CreateUdp2TcpObfuscator), - Settings::Shadowsocks(s) => shadowsocks::Shadowsocks::new(s) - .await - .map(box_obfuscator) - .map_err(Error::CreateShadowsocksObfuscator), - Settings::Quic(s) => quic::Quic::new(s) - .await - .map(box_obfuscator) - .map_err(Error::CreateQuicObfuscator), + Settings::Shadowsocks(s) => shadowsocks::Shadowsocks::new(s).await.map(box_obfuscator), + Settings::Quic(s) => quic::Quic::new(s).await.map(box_obfuscator), + Settings::Lwo(s) => lwo::Lwo::new(s).await.map(box_obfuscator), } } diff --git a/tunnel-obfuscation/src/lwo.rs b/tunnel-obfuscation/src/lwo.rs new file mode 100644 index 0000000000..54630dc299 --- /dev/null +++ b/tunnel-obfuscation/src/lwo.rs @@ -0,0 +1,366 @@ +//! LWO (Lightweight WireGuard Obfuscation) + +use std::{ + net::{Ipv4Addr, SocketAddr}, + sync::Arc, +}; + +use async_trait::async_trait; +use rand::{RngCore, SeedableRng}; +use talpid_types::net::wireguard::PublicKey; +use tokio::{io, net::UdpSocket}; +use tokio_util::sync::{CancellationToken, DropGuard}; + +use crate::{Obfuscator, socket::create_remote_socket}; + +const MAX_UDP_SIZE: usize = u16::MAX as usize; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failed to bind local UDP socket + #[error("Failed to bind client UDP socket")] + BindUdp(#[source] io::Error), + /// Failed to connect remote UDP socket + #[error("Failed to connect remote UDP socket")] + ConnectRemoteUdp(#[source] io::Error), + /// Missing UDP listener address + #[error("Failed to retrieve UDP socket bind address")] + GetUdpLocalAddress(#[source] io::Error), + /// Failed to get client sender address + #[error("Failed to retrieve client sender")] + PeekUdpSender(#[source] io::Error), +} + +#[derive(Debug)] +pub struct Settings { + /// Remote LWO/WG server + pub server_addr: SocketAddr, + /// Public key of the WG client + pub client_public_key: PublicKey, + /// Public key of the WG server + pub server_public_key: PublicKey, + /// Optional fwmark to set on the remote socket + #[cfg(target_os = "linux")] + pub fwmark: Option<u32>, +} + +pub struct Lwo { + task: tokio::task::JoinHandle<Result<(), Error>>, + local_endpoint: SocketAddr, + #[cfg(target_os = "android")] + wg_endpoint: Arc<UdpSocket>, + _drop_guard: DropGuard, +} + +impl Lwo { + pub async fn new(settings: &Settings) -> crate::Result<Self> { + let remote_socket = Arc::new( + create_remote_socket( + settings.server_addr.is_ipv4(), + #[cfg(target_os = "linux")] + settings.fwmark, + ) + .await?, + ); + let client_socket = Arc::new( + UdpSocket::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))) + .await + .map_err(Error::BindUdp) + .map_err(crate::Error::CreateLwoObfuscator)?, + ); + let local_endpoint = client_socket + .local_addr() + .map_err(Error::GetUdpLocalAddress) + .map_err(crate::Error::CreateLwoObfuscator)?; + + let rx_key = settings.client_public_key.clone(); + let tx_key = settings.server_public_key.clone(); + + let server_addr = settings.server_addr; + + let token = CancellationToken::new(); + let cancel_token = token.child_token(); + let _drop_guard = token.drop_guard(); + + #[cfg(target_os = "android")] + let wg_endpoint = remote_socket.clone(); + + let task = tokio::spawn(async move { + remote_socket + .connect(server_addr) + .await + .map_err(Error::ConnectRemoteUdp)?; + log::debug!("Connected to {server_addr}"); + + let client_addr = client_socket + .peek_sender() + .await + .map_err(Error::GetUdpLocalAddress)?; + client_socket + .connect(client_addr) + .await + .map_err(Error::PeekUdpSender)?; + log::debug!("Client socket connected to {client_addr}"); + + let rx_socket = client_socket.clone(); + let tx_socket = remote_socket.clone(); + let mut send_task = tokio::spawn(async move { + run_obfuscation(true, tx_key, rx_socket, tx_socket).await; + }); + + let rx_socket = remote_socket.clone(); + let tx_socket = client_socket.clone(); + let mut recv_task = tokio::spawn(async move { + run_obfuscation(false, rx_key, rx_socket, tx_socket).await; + }); + + tokio::select! { + _ = cancel_token.cancelled() => log::debug!("Stopping LWO obfuscation"), + _result = &mut recv_task => log::debug!("LWO client closed (recv_task)"), + _result = &mut send_task => log::debug!("LWO client closed (send_task)"), + }; + + send_task.abort(); + recv_task.abort(); + + Ok(()) + }); + + Ok(Self { + task, + local_endpoint, + #[cfg(target_os = "android")] + wg_endpoint, + _drop_guard, + }) + } +} + +async fn run_obfuscation( + sending: bool, + key: PublicKey, + read_socket: Arc<UdpSocket>, + write_socket: Arc<UdpSocket>, +) { + if sending { + let mut rng = new_rng(); + run_obfuscation_inner( + move |buf| obfuscate(&mut rng, buf, key.as_bytes()), + read_socket, + write_socket, + ) + .await + } else { + run_obfuscation_inner( + move |buf| deobfuscate(buf, key.as_bytes()), + read_socket, + write_socket, + ) + .await + } +} + +async fn run_obfuscation_inner( + mut action: impl FnMut(&mut [u8]), + read_socket: Arc<UdpSocket>, + write_socket: Arc<UdpSocket>, +) { + let mut buf = vec![0u8; MAX_UDP_SIZE]; + + loop { + let read_n = match read_socket.recv(&mut buf).await { + Ok(read_n) => read_n, + Err(err) => { + log::debug!("read_socket.recv failed: {err}"); + return; + } + }; + + // TODO: recv and send concurrently + action(&mut buf[..read_n]); + + if let Err(err) = write_socket.send(&buf[..read_n]).await { + log::debug!("write_socket.send_to failed: {err}"); + return; + } + } +} + +// WG message types, copied from boringtun +type MessageType = u8; +const HANDSHAKE_INIT: MessageType = 1; +const HANDSHAKE_RESP: MessageType = 2; +const COOKIE_REPLY: MessageType = 3; +const DATA: MessageType = 4; + +const HANDSHAKE_INIT_SZ: usize = 148; +const HANDSHAKE_RESP_SZ: usize = 92; +const COOKIE_REPLY_SZ: usize = 64; +const DATA_OVERHEAD_SZ: usize = 32; + +/// Bit to set in the second byte of the WG header to enable LWO +const OBFUSCATION_BIT: u8 = 0b10000000; + +pub fn obfuscate(rng: &mut impl RngCore, packet: &mut [u8], key: &[u8; 32]) { + let Some(header_bytes) = header_mut(packet, 0) else { + return; + }; + + xor_bytes(header_bytes, key); + + // randomize byte and set MSB + let rand_byte = (rng.next_u32() % u8::MAX as u32) as u8; + header_bytes[1] = rand_byte | OBFUSCATION_BIT; +} + +pub fn deobfuscate(packet: &mut [u8], key: &[u8; 32]) { + let Some(header_bytes) = header_mut(packet, key[0]) else { + return; + }; + #[cfg(debug_assertions)] + if !is_obfuscated(header_bytes[1]) { + log::error!("Received non-obfuscated packet from relay"); + return; + } + + xor_bytes(header_bytes, key); + + header_bytes[1] = 0; +} + +#[cfg(debug_assertions)] +const fn is_obfuscated(reserved_byte: u8) -> bool { + reserved_byte & OBFUSCATION_BIT != 0 +} + +fn header_mut(packet: &mut [u8], key_byte: u8) -> Option<&mut [u8]> { + let &header_type = packet.first()?; + match header_type ^ key_byte { + HANDSHAKE_INIT => packet.get_mut(..HANDSHAKE_INIT_SZ), + HANDSHAKE_RESP => packet.get_mut(..HANDSHAKE_RESP_SZ), + COOKIE_REPLY => packet.get_mut(..COOKIE_REPLY_SZ), + DATA => packet.get_mut(..DATA_OVERHEAD_SZ), + _ => None, + } +} + +fn xor_bytes(data: &mut [u8], key: &[u8]) { + data.iter_mut() + .zip(key.iter().cycle()) + .for_each(|(byte, key_byte)| *byte ^= key_byte); +} + +#[async_trait] +impl Obfuscator for Lwo { + fn endpoint(&self) -> SocketAddr { + self.local_endpoint + } + + async fn run(self: Box<Self>) -> crate::Result<()> { + match self.task.await { + Ok(result) => result.map_err(crate::Error::RunLwoObfuscator), + Err(_err) if _err.is_cancelled() => Ok(()), + Err(_err) => panic!("server handle panicked"), + } + } + + fn packet_overhead(&self) -> u16 { + 0 + } + + #[cfg(target_os = "android")] + fn remote_socket_fd(&self) -> std::os::unix::io::RawFd { + use std::os::fd::AsRawFd; + self.wg_endpoint.as_raw_fd() + } +} + +pub fn new_rng() -> impl RngCore { + rand::rngs::SmallRng::from_entropy() +} + +#[cfg(test)] +mod test { + use super::*; + + fn fake_packet() -> Vec<u8> { + let mut packet = vec![0u8; DATA_OVERHEAD_SZ + 100]; + packet[0] = DATA; + rand::thread_rng().fill_bytes(&mut packet[DATA_OVERHEAD_SZ..]); + packet + } + + #[test] + fn test_obfuscation() { + let key = [0xefu8; 32]; + let mut packet = fake_packet(); + let original_packet = packet.clone(); + + let mut rng = new_rng(); + + obfuscate(&mut rng, &mut packet, &key); + assert_ne!(packet, original_packet); + assert_eq!( + packet[DATA_OVERHEAD_SZ..], + original_packet[DATA_OVERHEAD_SZ..], + "payload should be unchanged" + ); + + deobfuscate(&mut packet, &key); + assert_eq!(packet, original_packet); + } + + #[tokio::test] + async fn test_e2e_obfuscation() { + let wg_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let endpoint = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + + let client_public_key = + PublicKey::from_base64("8Ka2l4T0tVrSR5pkcsvRG++mBlxfuf8XOxpqBkOCikU=").unwrap(); + let server_public_key = + PublicKey::from_base64("4EkA4c160oQgN/YaNR9GN3gLMevXEfx5hnlc9jYmw14=").unwrap(); + + let settings = Settings { + server_addr: endpoint.local_addr().unwrap(), + client_public_key: client_public_key.clone(), + server_public_key: server_public_key.clone(), + #[cfg(target_os = "linux")] + fwmark: None, + }; + + let lwo = Lwo::new(&settings).await.unwrap(); + let client_socket_addr = lwo.local_endpoint; + + tokio::spawn(Box::new(lwo).run()); + + let mut rng = new_rng(); + + // Send a test message, verify it on the server + let packet = fake_packet(); + + wg_socket + .send_to(&packet, client_socket_addr) + .await + .unwrap(); + + let mut buf = vec![0u8; 1500]; + let (n, addr) = endpoint.recv_from(&mut buf).await.unwrap(); + deobfuscate(&mut buf, server_public_key.as_bytes()); + assert_eq!(&buf[..n], packet); + + // Send a message to the client, verify it + let packet = fake_packet(); + + let mut obfuscated_packet = packet.clone(); + obfuscate( + &mut rng, + &mut obfuscated_packet, + client_public_key.as_bytes(), + ); + + endpoint.send_to(&obfuscated_packet, addr).await.unwrap(); + + let (n, _addr) = wg_socket.recv_from(&mut buf).await.unwrap(); + assert_eq!(&buf[..n], &packet); + } +} diff --git a/tunnel-obfuscation/src/quic.rs b/tunnel-obfuscation/src/quic.rs index 7cd4fcc305..8c587f4abd 100644 --- a/tunnel-obfuscation/src/quic.rs +++ b/tunnel-obfuscation/src/quic.rs @@ -9,7 +9,7 @@ use std::{ use tokio::net::UdpSocket; use tokio_util::sync::CancellationToken; -use crate::Obfuscator; +use crate::{Obfuscator, socket::create_remote_socket}; type Result<T> = std::result::Result<T, Error>; @@ -19,9 +19,6 @@ pub enum Error { BindError(#[source] io::Error), #[error("Masque proxy error")] MasqueProxyError(#[source] mullvad_masque_proxy::client::Error), - #[cfg(target_os = "linux")] - #[error("Failed to set fwmark on remote socket")] - Fwmark(#[source] io::Error), } #[derive(Debug)] @@ -121,42 +118,21 @@ impl std::str::FromStr for AuthToken { } impl Quic { - pub(crate) async fn new(settings: &Settings) -> Result<Self> { + pub(crate) async fn new(settings: &Settings) -> crate::Result<Self> { let (local_socket, local_udp_client_addr) = - Quic::create_local_udp_socket(settings.quic_endpoint.is_ipv4()).await?; + Quic::create_local_udp_socket(settings.quic_endpoint.is_ipv4()) + .await + .map_err(crate::Error::CreateQuicObfuscator)?; // The address family of the local QUIC client socket has to match the address family // of the endpoint we're connecting to. The address itself is not important to consumers wanting // to obfuscate traffic. It is solely used by the local proxy client to know where the QUIC // obfuscator is running. - let quic_client_local_addr = if settings.quic_endpoint.is_ipv4() { - SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)) - } else { - SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0)) - }; - let quic_socket = { - // family - let domain = match quic_client_local_addr { - SocketAddr::V4(_) => socket2::Domain::IPV4, - SocketAddr::V6(_) => socket2::Domain::IPV6, - }; - let ty = socket2::Type::DGRAM; - let protocol = Some(socket2::Protocol::UDP); - let socket = socket2::Socket::new(domain, ty, protocol).map_err(Error::BindError)?; - socket - .bind(&socket2::SockAddr::from(quic_client_local_addr)) - .map_err(Error::BindError)?; - + let quic_socket = create_remote_socket( + settings.quic_endpoint.is_ipv4(), #[cfg(target_os = "linux")] - if let Some(fwmark) = settings.fwmark { - socket.set_mark(fwmark).map_err(Error::Fwmark)?; - } - - // The caller is responsible for ensuring that the socket is in non-blocking mode - // when calling UdpSocket::from_std - socket.set_nonblocking(true).map_err(Error::BindError)?; - - UdpSocket::from_std(std::net::UdpSocket::from(socket)).map_err(Error::BindError)? - }; + settings.fwmark, + ) + .await?; let config_builder = ClientConfig::builder() .client_socket(local_socket) diff --git a/tunnel-obfuscation/src/shadowsocks.rs b/tunnel-obfuscation/src/shadowsocks.rs index 795b2f4065..081a442654 100644 --- a/tunnel-obfuscation/src/shadowsocks.rs +++ b/tunnel-obfuscation/src/shadowsocks.rs @@ -3,6 +3,8 @@ //! Note: It is important not to connect to the shadowsocks endpoint right away. The remote socket //! must be protected in `VpnService` so that the socket is not routed through the tunnel. +use crate::socket::create_remote_socket; + use super::Obfuscator; use async_trait::async_trait; use shadowsocks::{ @@ -18,9 +20,6 @@ use shadowsocks::{ use std::{io, net::SocketAddr, sync::Arc}; use tokio::{net::UdpSocket, sync::oneshot}; -#[cfg(target_os = "linux")] -use nix::sys::socket::{setsockopt, sockopt}; - #[cfg(target_os = "android")] use std::os::fd::AsRawFd; @@ -34,28 +33,12 @@ pub enum Error { /// Failed to bind local UDP socket #[error("Failed to bind UDP socket")] BindUdp(#[source] io::Error), - /// Failed to bind remote UDP socket - #[error("Failed to bind remote UDP socket")] - BindRemoteUdp(#[source] io::Error), - /// Failed to set fwmark - #[cfg(target_os = "linux")] - #[error("Failed to set fwmark")] - SetFwmark(#[source] nix::Error), /// Missing UDP listener address #[error("Failed to retrieve UDP socket bind address")] GetUdpLocalAddress(#[source] io::Error), /// Failed to wait for UDP client #[error("Failed to wait for UDP client")] WaitForUdpClient(#[source] io::Error), - /// Failed to create UDP stream - #[error("Failed to create UDP stream")] - CreateUdpStream(#[source] io::Error), - /// Failed to connect to Shadowsocks endpoint - #[error("Failed to connect to Shadowsocks endpoint")] - ConnectShadowsocks(#[from] io::Error), - /// Failed to receive remote socket descriptor - #[error("Failed to receive remote socket descriptor")] - ReceiveRemoteFd, } pub struct Shadowsocks { @@ -79,13 +62,15 @@ pub struct Settings { } impl Shadowsocks { - pub(crate) async fn new(settings: &Settings) -> Result<Self> { + pub(crate) async fn new(settings: &Settings) -> crate::Result<Self> { let (local_udp_socket, udp_client_addr) = - create_local_udp_socket(settings.shadowsocks_endpoint.is_ipv4()).await?; + create_local_udp_socket(settings.shadowsocks_endpoint.is_ipv4()) + .await + .map_err(crate::Error::CreateShadowsocksObfuscator)?; let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let remote_socket = create_shadowsocks_socket( + let remote_socket = create_remote_socket( settings.shadowsocks_endpoint.is_ipv4(), #[cfg(target_os = "linux")] settings.fwmark, @@ -169,26 +154,6 @@ fn connect_shadowsocks(remote_socket: UdpSocket, shadowsocks_endpoint: SocketAdd ProxySocket::from_socket(UdpSocketType::Client, ss_context, &ss_config, remote_socket) } -async fn create_shadowsocks_socket( - ipv4: bool, - #[cfg(target_os = "linux")] fwmark: Option<u32>, -) -> std::result::Result<UdpSocket, Error> { - let random_bind_addr = if ipv4 { - SocketAddr::new("0.0.0.0".parse().unwrap(), 0) - } else { - SocketAddr::new("::".parse().unwrap(), 0) - }; - let socket = UdpSocket::bind(random_bind_addr) - .await - .map_err(Error::BindRemoteUdp)?; - #[cfg(target_os = "linux")] - if let Some(fwmark) = fwmark { - setsockopt(&socket, sockopt::Mark, &fwmark).map_err(Error::SetFwmark)?; - } - - Ok(socket) -} - async fn create_local_udp_socket(ipv4: bool) -> Result<(UdpSocket, SocketAddr)> { let random_bind_addr = if ipv4 { SocketAddr::new("127.0.0.1".parse().unwrap(), 0) diff --git a/tunnel-obfuscation/src/socket.rs b/tunnel-obfuscation/src/socket.rs new file mode 100644 index 0000000000..727a648ba4 --- /dev/null +++ b/tunnel-obfuscation/src/socket.rs @@ -0,0 +1,26 @@ +use std::net::SocketAddr; +use tokio::net::UdpSocket; + +use crate::Error; + +pub async fn create_remote_socket( + ipv4: bool, + #[cfg(target_os = "linux")] fwmark: Option<u32>, +) -> Result<UdpSocket, Error> { + let random_bind_addr = if ipv4 { + SocketAddr::new("0.0.0.0".parse().unwrap(), 0) + } else { + SocketAddr::new("::".parse().unwrap(), 0) + }; + let socket = UdpSocket::bind(random_bind_addr) + .await + .map_err(Error::BindRemoteUdp)?; + #[cfg(target_os = "linux")] + if let Some(fwmark) = fwmark { + use nix::sys::socket::{setsockopt, sockopt}; + + setsockopt(&socket, sockopt::Mark, &fwmark).map_err(Error::SetFwmark)?; + } + + Ok(socket) +} |
