summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-09-05 13:02:33 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-09-15 09:21:28 +0200
commit344b9291ba15b6609d79798d7e21997f97fbc4d9 (patch)
tree42bda66a2f74afda53cb7a73ab87f8948de4b72a
parent29ba9088210475eb179c8eefe5c9f3b8bbc92583 (diff)
downloadmullvadvpn-344b9291ba15b6609d79798d7e21997f97fbc4d9.tar.xz
mullvadvpn-344b9291ba15b6609d79798d7e21997f97fbc4d9.zip
Add LWO obfuscator
-rw-r--r--Cargo.lock6
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt4
-rw-r--r--mullvad-management-interface/proto/management_interface.proto2
-rw-r--r--mullvad-management-interface/src/types/conversions/net.rs6
-rw-r--r--mullvad-management-interface/src/types/conversions/relay_constraints.rs2
-rw-r--r--mullvad-relay-selector/src/relay_selector/helpers.rs21
-rw-r--r--mullvad-relay-selector/src/relay_selector/matcher.rs2
-rw-r--r--mullvad-relay-selector/src/relay_selector/mod.rs1
-rw-r--r--mullvad-relay-selector/src/relay_selector/query.rs6
-rw-r--r--mullvad-relay-selector/tests/relay_selector.rs3
-rw-r--r--mullvad-types/src/relay_constraints.rs2
-rw-r--r--talpid-types/src/net/mod.rs3
-rw-r--r--talpid-types/src/net/obfuscation.rs7
-rw-r--r--talpid-wireguard/src/obfuscation.rs15
-rw-r--r--tunnel-obfuscation/Cargo.toml11
-rw-r--r--tunnel-obfuscation/benches/lwo.rs61
-rw-r--r--tunnel-obfuscation/src/lib.rs28
-rw-r--r--tunnel-obfuscation/src/lwo.rs366
-rw-r--r--tunnel-obfuscation/src/quic.rs44
-rw-r--r--tunnel-obfuscation/src/shadowsocks.rs49
-rw-r--r--tunnel-obfuscation/src/socket.rs26
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)
+}