From fcc75e977274681fe4e00f96bd2645a313360096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Em=C4=ABls?= Date: Mon, 9 Sep 2024 14:47:54 +0200 Subject: [PATCH] Add mullvad-obfuscated-dns-proxy --- Cargo.lock | 27 +++ Cargo.toml | 1 + mullvad-encrypted-dns-proxy/Cargo.toml | 22 +++ .../examples/forwarder.rs | 33 ++++ mullvad-encrypted-dns-proxy/src/config/mod.rs | 129 +++++++++++++ .../src/config/plain.rs | 100 ++++++++++ mullvad-encrypted-dns-proxy/src/config/xor.rs | 162 ++++++++++++++++ .../src/config_resolver.rs | 112 +++++++++++ .../src/forwarder/mod.rs | 175 ++++++++++++++++++ mullvad-encrypted-dns-proxy/src/lib.rs | 7 + mullvad-ios/Cargo.toml | 1 + 11 files changed, 769 insertions(+) create mode 100644 mullvad-encrypted-dns-proxy/Cargo.toml create mode 100644 mullvad-encrypted-dns-proxy/examples/forwarder.rs create mode 100644 mullvad-encrypted-dns-proxy/src/config/mod.rs create mode 100644 mullvad-encrypted-dns-proxy/src/config/plain.rs create mode 100644 mullvad-encrypted-dns-proxy/src/config/xor.rs create mode 100644 mullvad-encrypted-dns-proxy/src/config_resolver.rs create mode 100644 mullvad-encrypted-dns-proxy/src/forwarder/mod.rs create mode 100644 mullvad-encrypted-dns-proxy/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c0c8329eed39..1611210b9ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,20 +1340,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" dependencies = [ "async-trait", + "bytes", "cfg-if", "data-encoding", "enum-as-inner", "futures-channel", "futures-io", "futures-util", + "h2 0.3.26", + "http 0.2.12", "idna 0.4.0", "ipnet", "once_cell", "rand 0.8.5", + "rustls", + "rustls-pemfile", "serde", "thiserror", "tinyvec", "tokio", + "tokio-rustls", "tracing", "url", ] @@ -1373,10 +1379,12 @@ dependencies = [ "parking_lot", "rand 0.8.5", "resolv-conf", + "rustls", "serde", "smallvec", "thiserror", "tokio", + "tokio-rustls", "tracing", ] @@ -2378,6 +2386,18 @@ dependencies = [ "winres", ] +[[package]] +name = "mullvad-encrypted-dns-proxy" +version = "0.0.0" +dependencies = [ + "byteorder", + "hickory-resolver", + "log", + "rustls", + "tokio", + "webpki-roots", +] + [[package]] name = "mullvad-exclude" version = "0.0.0" @@ -2404,6 +2424,7 @@ dependencies = [ "cbindgen", "libc", "log", + "mullvad-encrypted-dns-proxy", "oslog", "shadowsocks-service", "talpid-tunnel-config-client", @@ -4853,6 +4874,12 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "which" version = "4.4.2" diff --git a/Cargo.toml b/Cargo.toml index 728799f73903..0b674d289be5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "mullvad-jni", "mullvad-management-interface", "mullvad-nsis", + "mullvad-encrypted-dns-proxy", "mullvad-paths", "mullvad-problem-report", "mullvad-relay-selector", diff --git a/mullvad-encrypted-dns-proxy/Cargo.toml b/mullvad-encrypted-dns-proxy/Cargo.toml new file mode 100644 index 000000000000..7b97bdfa627e --- /dev/null +++ b/mullvad-encrypted-dns-proxy/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "mullvad-encrypted-dns-proxy" +description = "A port forwarding proxy that retrieves its configuration from a AAAA record over DoH" +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +tokio = { workspace = true, features = [ "macros" ] } +log = { workspace = true } +byteorder = "1" +hickory-resolver = { version = "0.24.1", features = [ "dns-over-https-rustls" ]} +webpki-roots = "0.25.0" +rustls = "0.21" + +[dev-dependencies] +tokio = { workspace = true, features = [ "full" ]} diff --git a/mullvad-encrypted-dns-proxy/examples/forwarder.rs b/mullvad-encrypted-dns-proxy/examples/forwarder.rs new file mode 100644 index 000000000000..caf97a893fb5 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/examples/forwarder.rs @@ -0,0 +1,33 @@ +use std::env::args; + +use mullvad_encrypted_dns_proxy::{config::Obfuscator, config_resolver, forwarder}; +use tokio::net::TcpListener; + +/// This can be tested out by using curl: +/// `curl https://api.mullvad.net:$port/api/v1/relays --resolve api.mullvad.net:$port:$addr` +/// where $addr and $port are the listening address of the proxy (bind_addr). +#[tokio::main] +async fn main() { + let mut configs = + config_resolver::resolve_configs(config_resolver::default_resolvers(), "frakta.eu") + .await + .expect("Failed to resolve configs"); + + let bind_addr = args().nth(1).unwrap_or("127.0.0.1:0".to_string()); + let obfuscator = configs.xor.pop().expect("No XOR config"); + println!("Obfuscator in use - {:?}", obfuscator); + let obfuscator: Box = Box::new(obfuscator); + let listener = TcpListener::bind(bind_addr) + .await + .expect("Failed to bind listener socket"); + let listen_addr = listener + .local_addr() + .expect("failed to obtain listen address"); + println!("Listening on {listen_addr}"); + while let Ok((client_conn, _client_addr)) = listener.accept().await { + let connected = crate::forwarder::Forwarder::connect(obfuscator.clone()) + .await + .expect("failed to connect to obfuscator"); + let _ = connected.forward(client_conn).await; + } +} diff --git a/mullvad-encrypted-dns-proxy/src/config/mod.rs b/mullvad-encrypted-dns-proxy/src/config/mod.rs new file mode 100644 index 000000000000..db967397d322 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/config/mod.rs @@ -0,0 +1,129 @@ +//! Parse and use various proxy configurations as they are retrieved via AAAA records, hopefully +//! served by DoH resolvers. +use std::{ + io::Cursor, + net::{Ipv6Addr, SocketAddrV4}, +}; + +use byteorder::{LittleEndian, ReadBytesExt}; + +mod plain; +mod xor; +pub use plain::Plain; +pub use xor::Xor; + +/// An error that happens when parsing IPv6 addresses into proxy configurations. +#[derive(Debug)] +pub enum Error { + /// IP address representing a Xor proxy was not valid + InvalidXor(xor::Error), + /// IP address representing the plain proxy was not valid + InvalidPlain(plain::Error), + /// IP addresses did not contain any valid proxy configuration + NoProxies, +} + +/// If a given IPv6 address does not contain a valid value for the proxy version, this error type +/// will contain the unrecognized value. +#[derive(Debug)] +pub struct ErrorUnknownType(u16); + +/// Type of a proxy configuration. Derived from the 2nd hextet of an IPv6 address in network byte +/// order. E.g. an IPv6 address such as `7f7f:2323::` would have a proxy type value of `0x2323`. +#[derive(PartialEq, Debug)] +#[repr(u16)] +enum ProxyType { + Plain = 0x01, + XorV1 = 0x02, + XorV2 = 0x03, +} + +impl TryFrom for ProxyType { + type Error = ErrorUnknownType; + + /// A proxy type is represented by the second hexlet in an IPv6 address, and it is to be + /// interpreted as little endian. All other data is disregarded. + fn try_from(value: Ipv6Addr) -> Result { + let mut data = Cursor::new(value.octets()); + // skip the first 2 bytes since it's just padding to make the IP look more like a legit + // IPv6 address. + + data.set_position(2); + match data + .read_u16::() + .expect("IPv6 must have at least 16 bytes") + { + 0x01 => Ok(Self::Plain), + 0x02 => Ok(Self::XorV1), + 0x03 => Ok(Self::XorV2), + unknown => Err(ErrorUnknownType(unknown)), + } + } +} + +/// Contains valid proxy configurations as derived from a set of IPv6 addresses. +pub struct AvailableProxies { + /// Plain proxies just forward traffic without any obfuscation. + pub plain: Vec, + /// Xor proxies xor a pre-shared key with all the traffic. + pub xor: Vec, +} + +impl TryFrom> for AvailableProxies { + type Error = Error; + + fn try_from(ips: Vec) -> Result { + let mut proxies = AvailableProxies { + plain: vec![], + xor: vec![], + }; + + for ip in ips { + match ProxyType::try_from(ip) { + Ok(ProxyType::Plain) => { + proxies + .plain + .push(Plain::try_from(ip).map_err(Error::InvalidPlain)?); + } + Ok(ProxyType::XorV2) => { + proxies + .xor + .push(Xor::try_from(ip).map_err(Error::InvalidXor)?); + } + + // V1 types are ignored and so are errors + Ok(ProxyType::XorV1) => continue, + + Err(ErrorUnknownType(unknown_proxy_type)) => { + log::error!("Unknown proxy type {unknown_proxy_type}"); + } + } + } + if proxies.plain.is_empty() && proxies.xor.is_empty() { + return Err(Error::NoProxies); + } + + Ok(proxies) + } +} + +/// A trait that can be used by a forwarder to forward traffic. +pub trait Obfuscator: Send { + /// Provides the endpoint for the proxy. This address must be connected and all traffic to it + /// should first be obfuscated with `Obfuscator::obfuscate`. + fn addr(&self) -> SocketAddrV4; + /// Applies obfuscation to a given buffer of bytes. + fn obfuscate(&mut self, buffer: &mut [u8]); + /// Constructs a new obfuscator of the same type and configuration, with it's internal state + /// reset. + fn clone(&self) -> Box; +} + +#[test] +fn wrong_proxy_type() { + let addr: Ipv6Addr = "ffff:2345::".parse().unwrap(); + match ProxyType::try_from(addr) { + Err(ErrorUnknownType(0x4523)) => (), + anything_else => panic!("Expected unknown type 0x33, got {anything_else:x?}"), + } +} diff --git a/mullvad-encrypted-dns-proxy/src/config/plain.rs b/mullvad-encrypted-dns-proxy/src/config/plain.rs new file mode 100644 index 000000000000..fc750cdc17f4 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/config/plain.rs @@ -0,0 +1,100 @@ +use byteorder::{LittleEndian, ReadBytesExt}; +use std::{ + io::{Cursor, Read}, + net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}, +}; + +/// Obfuscator that does not obfuscate. It still can circumvent censorship since it is reaching our +/// API through a different IP address. +/// +/// A plain configuration is represented by proxy type ProxyType::Plain (0x01). A plain +/// configuration interprets the following bytes from a given IPv6 address: +/// bytes 4-8 - u16le - proxy type - must be 0x0001 +/// bytes 8-16 - [u8; 4] - 4 bytes representing the proxy IPv4 address +/// bytes 16-18 - u16le - port on which the proxy is listening +/// +/// Given the above, an IPv6 address `2001:100:b9d5:9a75:3804::` will have the second hexlet +/// (0x0100) represent the proxy type, the following 2 hexlets (0xb9d5, 0x9a75) - the IPv4 address +/// of the proxy endpoint, and the final hexlet represents the port for the proxy endpoint - the +/// remaining bytes can be ignored. +#[derive(PartialEq, Debug, Clone)] +pub struct Plain { + pub addr: SocketAddrV4, +} + +#[derive(Debug)] +pub enum Error { + UnexpectedType(u16), +} + +impl TryFrom for Plain { + type Error = Error; + + fn try_from(ip: Ipv6Addr) -> Result { + let mut cursor = Cursor::new(ip.octets()); + + // skip the first 2 bytes since it's just padding to make the IP look more like a legit + // IPv6 address. + cursor.set_position(2); + let proxy_type = cursor.read_u16::().unwrap(); + if proxy_type != super::ProxyType::Plain as u16 { + return Err(Error::UnexpectedType(proxy_type)); + } + + let mut ipv4_bytes = [0u8; 4]; + cursor.read_exact(&mut ipv4_bytes).unwrap(); + let v4_addr = Ipv4Addr::from(ipv4_bytes); + + let port = cursor.read_u16::().unwrap(); + + Ok(Self { + addr: SocketAddrV4::new(v4_addr, port), + }) + } +} + +impl super::Obfuscator for Plain { + // can be a noop, since this configuration is just a port forward. + fn obfuscate(&mut self, _buffer: &mut [u8]) {} + + fn addr(&self) -> SocketAddrV4 { + self.addr + } + + fn clone(&self) -> Box { + Box::new(Clone::clone(self)) + } +} + +#[test] +fn test_parsing() { + struct Test { + input: Ipv6Addr, + expected: Plain, + } + let tests = vec![ + Test { + input: "2001:100:7f00:1:3905::".parse::().unwrap(), + expected: Plain { + addr: "127.0.0.1:1337".parse::().unwrap(), + }, + }, + Test { + input: "2001:100:c0a8:101:bb01::".parse::().unwrap(), + expected: Plain { + addr: "192.168.1.1:443".parse::().unwrap(), + }, + }, + Test { + input: "2001:100:c0a8:101:bb01:404::".parse::().unwrap(), + expected: Plain { + addr: "192.168.1.1:443".parse::().unwrap(), + }, + }, + ]; + + for t in tests { + let parsed = Plain::try_from(t.input).unwrap(); + assert_eq!(parsed, t.expected); + } +} diff --git a/mullvad-encrypted-dns-proxy/src/config/xor.rs b/mullvad-encrypted-dns-proxy/src/config/xor.rs new file mode 100644 index 000000000000..330a708f59f6 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/config/xor.rs @@ -0,0 +1,162 @@ +use byteorder::{LittleEndian, ReadBytesExt}; +use std::{ + io::{Cursor, Read}, + net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}, +}; + +use crate::config::Obfuscator; + +/// An obfuscator that XORs all traffic with the given key. +/// +/// A Xor configuration is represented by the proxy type ProxyType::XorV2 (0x03). There used to be a XorV1 (0x02), but it shouldn't be used. +/// The following bytes of an IPv6 address are interpreted to derive a Xor configuration: +/// bytes 4-8 - u16le - proxy type - must be 0x0003 +/// bytes 8-16 - [u8; 4] - v4 proxy address bytes +/// bytes 16-18 - u16le - port for the proxy socket address +/// bytes 18-24 - [u8; 6] - xor key bytes. 0x00 marks a premature end of the key +/// Given the above, `2001:300:b9d5:9a75:3a04:eafd:1100:ad9e` will have the second hexlet (0x0300) +/// represent the proxy type, the next 2 hexlets (0xb9d5,0x9a75) represent the IPv4 address for the +/// proxy endpoint, the next hexlet`3a04` represents the port for the proxy endpoint, and +/// the final 3 hexlets `eafd:1100:ad9e` represent the xor key (0xEA, 0xFD, 0x11). +#[derive(PartialEq, Debug)] +pub struct Xor { + addr: SocketAddrV4, + // the key to be used for Xor + xor_key: Vec, + key_index: usize, +} + +#[derive(Debug)] +pub enum Error { + EmptyXorKey, + UnexpectedType(u16), +} + +impl TryFrom for Xor { + type Error = Error; + + fn try_from(ip: Ipv6Addr) -> Result { + let mut cursor = Cursor::new(ip.octets()); + + cursor.set_position(2); + let proxy_type = cursor.read_u16::().unwrap(); + if proxy_type != super::ProxyType::XorV2 as u16 { + return Err(Error::UnexpectedType(proxy_type)); + } + + let mut ipv4_bytes = [0u8; 4]; + cursor.read_exact(&mut ipv4_bytes).unwrap(); + let v4_addr = Ipv4Addr::from(ipv4_bytes); + + let port = cursor.read_u16::().unwrap(); + + let mut key_bytes = [0u8; 6]; + cursor.read_exact(&mut key_bytes).unwrap(); + let xor_key = key_bytes + .into_iter() + .take_while(|byte| *byte != 0x00) + .collect::>(); + if xor_key.is_empty() { + return Err(Error::EmptyXorKey); + } + + Ok(Self { + addr: SocketAddrV4::new(v4_addr, port), + xor_key, + key_index: 0, + }) + } +} + +impl Obfuscator for Xor { + fn addr(&self) -> SocketAddrV4 { + self.addr + } + + fn obfuscate(&mut self, buffer: &mut [u8]) { + for byte in buffer.iter_mut() { + *byte ^= self.xor_key[self.key_index % self.xor_key.len()]; + self.key_index = (self.key_index + 1) % self.xor_key.len(); + } + } + + fn clone(&self) -> Box { + Box::new(Self { + xor_key: self.xor_key.clone(), + addr: self.addr, + key_index: 0, + }) + } +} + +#[test] +fn test_xor_parsing() { + struct Test { + input: Ipv6Addr, + expected: Xor, + } + let tests = vec![ + Test { + input: "2001:300:7f00:1:3905:0102:304:506" + .parse::() + .unwrap(), + expected: Xor { + addr: "127.0.0.1:1337".parse::().unwrap(), + xor_key: vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06], + key_index: 0, + }, + }, + Test { + input: "2001:300:7f00:1:3905:0100:304:506" + .parse::() + .unwrap(), + expected: Xor { + addr: "127.0.0.1:1337".parse::().unwrap(), + xor_key: vec![0x01], + key_index: 0, + }, + }, + Test { + input: "2001:300:c0a8:101:bb01:ff04:204:0" + .parse::() + .unwrap(), + expected: Xor { + addr: "192.168.1.1:443".parse::().unwrap(), + xor_key: vec![0xff, 0x04, 0x02, 0x04], + key_index: 0, + }, + }, + ]; + + for t in tests { + let parsed = Xor::try_from(t.input).unwrap(); + assert_eq!(parsed, t.expected); + } +} + +#[test] +fn test_obfuscation() { + let input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let mut payload = input.to_vec(); + let mut xor = Xor { + addr: "192.168.1.1:443".parse::().unwrap(), + xor_key: vec![0xff, 0x04, 0x02, 0x04], + key_index: 0, + }; + let mut dexor = xor.clone(); + xor.obfuscate(&mut payload); + dexor.obfuscate(&mut payload); + assert_eq!(input, payload.as_slice()); +} + +// Before XOR-v2 there was XOR-v1, which is now deprecated. This test verifies that the old Xor +// config does not deserialize. +#[test] +fn test_old_xor_addr() { + let _ = Xor::try_from( + "2001:200:7f00:1:3905:0102:304:506" + .parse::() + .unwrap(), + ) + .unwrap_err(); +} diff --git a/mullvad-encrypted-dns-proxy/src/config_resolver.rs b/mullvad-encrypted-dns-proxy/src/config_resolver.rs new file mode 100644 index 000000000000..2b0ede61ae01 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/config_resolver.rs @@ -0,0 +1,112 @@ +//! Resolve valid proxy configurations via DoH. +//! +use crate::config; +use hickory_resolver::{config::*, error::ResolveError, TokioAsyncResolver}; +use rustls::ClientConfig; +use std::{net::IpAddr, sync::Arc}; + +pub struct Nameserver { + pub name: String, + pub addr: Vec, +} + +#[derive(Debug)] +pub enum Error { + ResolutionError(ResolveError), + ParsingError(config::Error), +} + +pub fn default_resolvers() -> Vec { + vec![ + Nameserver { + name: "one.one.one.one".to_string(), + addr: vec!["1.1.1.1".parse().unwrap(), "1.0.0.1".parse().unwrap()], + }, + Nameserver { + name: "dns.google".to_string(), + addr: vec!["8.8.8.8".parse().unwrap(), "8.8.4.4".parse().unwrap()], + }, + Nameserver { + name: "dns.quad9.net".to_string(), + addr: vec![ + "9.9.9.9".parse().unwrap(), + "149.112.112.112".parse().unwrap(), + ], + }, + ] +} + +pub async fn resolve_configs( + resolvers: Vec, + domain: &str, +) -> Result { + let mut resolver_config = ResolverConfig::new(); + for resolver in resolvers.into_iter() { + let ns_config_group = + NameServerConfigGroup::from_ips_https(&resolver.addr, 443, resolver.name, false) + .into_inner(); + for ns_config in ns_config_group { + resolver_config.add_name_server(ns_config); + } + } + + resolver_config.set_tls_client_config(Arc::new(client_config_tls12())); + + resolve_config_with_resolverconfig(resolver_config, Default::default(), domain).await +} + +pub async fn resolve_config_with_resolverconfig( + resolver_config: ResolverConfig, + options: ResolverOpts, + domain: &str, +) -> Result { + let resolver = TokioAsyncResolver::tokio(resolver_config, options); + let lookup = resolver + .ipv6_lookup(domain) + .await + .map_err(Error::ResolutionError)?; + + let addrs = lookup + .into_iter() + .map(|aaaa_record| aaaa_record.0) + .collect::>(); + + config::AvailableProxies::try_from(addrs).map_err(Error::ParsingError) +} + +fn client_config_tls12() -> ClientConfig { + use rustls::RootCertStore; + let mut root_store = RootCertStore::empty(); + root_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| { + rustls::OwnedTrustAnchor::from_subject_spki_name_constraints( + ta.subject, + ta.spki, + ta.name_constraints, + ) + })); + + ClientConfig::builder() + .with_safe_default_cipher_suites() + .with_safe_default_kx_groups() + .with_safe_default_protocol_versions() // this enables TLS 1.2 and 1.3 + .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth() +} + +#[cfg(test)] +#[tokio::test] +async fn test_resolution() { + let nameservers = vec![Nameserver { + addr: vec!["1.1.1.1".parse().unwrap()], + name: "one.one.one.one".to_string(), + }]; + + let _ = resolve_configs(nameservers, "frakta.eu").await.unwrap(); +} + +#[cfg(test)] +#[test] +fn default_resolvers_dont_panic() { + let _ = default_resolvers(); +} diff --git a/mullvad-encrypted-dns-proxy/src/forwarder/mod.rs b/mullvad-encrypted-dns-proxy/src/forwarder/mod.rs new file mode 100644 index 000000000000..7ae905525ad6 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/forwarder/mod.rs @@ -0,0 +1,175 @@ +//! Forward TCP traffic over various proxy configurations. +use std::{ + io, + task::{ready, Poll}, +}; + +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; + +use crate::config::Obfuscator; + +/// Forwards local traffic to a proxy endpoint, obfuscating it. +pub struct Forwarder { + read_obfuscator: Box, + write_obfuscator: Box, + server_connection: TcpStream, +} + +impl Forwarder { + /// Create a forwarder that will connect to a given proxy endpoint. + pub async fn connect(obfuscator: Box) -> io::Result { + let server_connection = TcpStream::connect(obfuscator.addr()).await?; + + Ok(Self { + read_obfuscator: obfuscator.clone(), + write_obfuscator: obfuscator, + server_connection, + }) + } + + /// Forwards traffic from the client stream to the remote proxy, obfuscating and deobfuscating + /// it in the process. + pub async fn forward(self, client_stream: TcpStream) { + let (server_read, server_write) = self.server_connection.into_split(); + let (client_read, client_write) = client_stream.into_split(); + let _ = tokio::join!( + forward(self.read_obfuscator, client_read, server_write), + forward(self.write_obfuscator, server_read, client_write) + ); + } +} + +impl tokio::io::AsyncRead for Forwarder { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + let socket = std::pin::pin!(&mut self.server_connection); + match ready!(socket.poll_read(cx, buf)) { + // in this case, we can read and deobfuscate. + Ok(()) => { + self.read_obfuscator.obfuscate(buf.filled_mut()); + Poll::Ready(Ok(())) + } + Err(err) => Poll::Ready(Err(err)), + } + } +} + +impl tokio::io::AsyncWrite for Forwarder { + fn poll_write( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let socket = std::pin::pin!(&mut self.server_connection); + if let Err(err) = ready!(socket.poll_write_ready(cx)) { + return Poll::Ready(Err(err)); + }; + + let mut owned_buf = buf.to_vec(); + self.write_obfuscator.obfuscate(owned_buf.as_mut_slice()); + let socket = std::pin::pin!(&mut self.server_connection); + socket.poll_write(cx, &owned_buf) + } + + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + std::pin::pin!(&mut self.server_connection).poll_flush(cx) + } + + fn poll_shutdown( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + std::pin::pin!(&mut self.server_connection).poll_shutdown(cx) + } +} + +async fn forward( + mut obfuscator: Box, + mut source: impl AsyncRead + Unpin, + mut sink: impl AsyncWrite + Unpin, +) -> io::Result<()> { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + let mut buf = vec![0u8; 1024 * 64]; + while let Ok(n_bytes_read) = AsyncReadExt::read(&mut source, &mut buf).await { + if n_bytes_read == 0 { + break; + } + let bytes_received = &mut buf[..n_bytes_read]; + + obfuscator.obfuscate(bytes_received); + sink.write_all(bytes_received).await?; + } + Ok(()) +} + +// Constructs a server and a client, uses the Xor obfuscator to forward some bytes between to see +// the obfuscation works. +#[tokio::test] +async fn test_async_methods() { + use std::net::Ipv6Addr; + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + }; + let server_listener = + tokio::net::TcpListener::bind("127.0.0.1:0".parse::().unwrap()) + .await + .unwrap(); + let listener_addr = server_listener.local_addr().unwrap(); + let xor_key: &[u8] = &[0x01, 0x02, 0x03, 0x04, 0x00, 0x00]; + let address_bytes: &[u8] = &[127, 0, 0, 1]; + let port: &[u8] = &listener_addr.port().to_ne_bytes(); + + // 0x2001 - bogus IPv6 bytes + // 0x0300 - XOR proxy type + let mut ipv6_bytes = vec![0x20, 0x01, 0x03, 0x00]; + ipv6_bytes.extend_from_slice(address_bytes); + ipv6_bytes.extend_from_slice(port); + ipv6_bytes.extend_from_slice(xor_key); + let mut ipv6_buf = [0u8; 16]; + ipv6_buf.copy_from_slice(&ipv6_bytes); + + let ipv6 = Ipv6Addr::from(ipv6_buf); + + let xor = crate::config::Xor::try_from(ipv6).unwrap(); + let mut client_read_xor = Obfuscator::clone(&xor); + let mut client_write_xor = Obfuscator::clone(&xor); + let server_xor = Obfuscator::clone(&xor); + + // Server future - receives one TCP connection, then echos everything it reads from it back to + // the client, using obfuscation via the forwarder in both cases. + tokio::spawn(async move { + let (client_conn, _) = server_listener.accept().await.unwrap(); + let mut forwarder = Forwarder { + read_obfuscator: server_xor.clone(), + write_obfuscator: server_xor, + server_connection: client_conn, + }; + let mut buf = vec![0u8; 1024]; + while let Ok(bytes_read) = forwarder.read(&mut buf).await { + forwarder.write_all(&buf[..bytes_read]).await.unwrap(); + } + }); + + let mut client_connection = TcpStream::connect(listener_addr).await.unwrap(); + + for _ in 0..5 { + let original_payload = (1..127).collect::>(); + let mut payload = original_payload.clone(); + client_write_xor.obfuscate(payload.as_mut_slice()); + client_connection.write_all(&payload).await.unwrap(); + let mut read_buf = vec![0u8; payload.len()]; + client_connection.read_exact(&mut read_buf).await.unwrap(); + client_read_xor.obfuscate(&mut read_buf); + assert_eq!(original_payload, read_buf); + } +} diff --git a/mullvad-encrypted-dns-proxy/src/lib.rs b/mullvad-encrypted-dns-proxy/src/lib.rs new file mode 100644 index 000000000000..e8efee0806a2 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/lib.rs @@ -0,0 +1,7 @@ +//! Crate for creating port forwarding proxies for TCP connections, with some varying amount of +//! obfuscation. +//! + +pub mod config; +pub mod config_resolver; +pub mod forwarder; diff --git a/mullvad-ios/Cargo.toml b/mullvad-ios/Cargo.toml index 6e0fedb8df41..83d3af744750 100644 --- a/mullvad-ios/Cargo.toml +++ b/mullvad-ios/Cargo.toml @@ -20,6 +20,7 @@ tunnel-obfuscation = { path = "../tunnel-obfuscation" } oslog = "0.2" talpid-types = { path = "../talpid-types" } talpid-tunnel-config-client = { path = "../talpid-tunnel-config-client" } +mullvad-encrypted-dns-proxy = { path = "../mullvad-encrypted-dns-proxy" } shadowsocks-service = { workspace = true, features = [ "local",