From f11e8b493891ad0af760d20deae3ae3e47df4849 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 | 21 ++ .../examples/forwarder.rs | 42 ++++ mullvad-encrypted-dns-proxy/src/config/mod.rs | 124 +++++++++++ .../src/config/plain.rs | 79 +++++++ mullvad-encrypted-dns-proxy/src/config/xor.rs | 204 ++++++++++++++++++ .../src/config_resolver.rs | 140 ++++++++++++ mullvad-encrypted-dns-proxy/src/forwarder.rs | 194 +++++++++++++++++ mullvad-encrypted-dns-proxy/src/lib.rs | 13 ++ mullvad-ios/Cargo.toml | 1 + 11 files changed, 846 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.rs create mode 100644 mullvad-encrypted-dns-proxy/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c0c8329eed39..ed52bbb2f8e3 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 = [ + "env_logger 0.10.2", + "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..4f101e3ed075 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/Cargo.toml @@ -0,0 +1,21 @@ +[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 } +hickory-resolver = { version = "0.24.1", features = [ "dns-over-https-rustls" ]} +webpki-roots = "0.25.0" +rustls = "0.21" + +[dev-dependencies] +env_logger = { workspace = true } diff --git a/mullvad-encrypted-dns-proxy/examples/forwarder.rs b/mullvad-encrypted-dns-proxy/examples/forwarder.rs new file mode 100644 index 000000000000..00302886a2c5 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/examples/forwarder.rs @@ -0,0 +1,42 @@ +use std::env::args; + +use mullvad_encrypted_dns_proxy::{config_resolver, Forwarder}; +use tokio::net::TcpListener; + +/// This can be tested out by using curl: +/// `curl https://api.mullvad.net:$port/app/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() { + env_logger::init(); + + let bind_addr = args().nth(1).unwrap_or("127.0.0.1:0".to_owned()); + + let resolvers = config_resolver::default_resolvers(); + let configs = config_resolver::resolve_configs(&resolvers, "frakta.eu") + .await + .expect("Failed to resolve configs"); + + let proxy_config = configs + .into_iter() + .find(|c| c.obfuscation.is_some()) + .expect("No XOR config"); + println!("Proxy config in use: {:?}", proxy_config); + + 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 { + println!("Incoming connection from {client_addr}"); + let connected = Forwarder::connect(&proxy_config) + .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..cd93d2989e43 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/config/mod.rs @@ -0,0 +1,124 @@ +//! Parse and use various proxy configurations as they are retrieved via AAAA records, hopefully +//! served by DoH resolvers. + +use core::fmt; +use std::net::{Ipv6Addr, SocketAddrV4}; + +mod plain; +mod xor; + +pub use xor::XorKey; + +/// All the errors that can happen during deserialization of a [`ProxyConfig`]. +#[derive(Debug, Eq, PartialEq)] +pub enum Error { + /// The proxy type field has a value this library is not compatible with + UnknownProxyType(u16), + /// The XorV1 proxy type is deprecated and not supported + XorV1Unsupported, + /// The port is not valid + InvalidPort(u16), + /// The key to use for XOR obfuscation was empty (all zeros) + EmptyXorKey, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnknownProxyType(t) => write!(f, "Unknown type of proxy: {t:#x}"), + Self::XorV1Unsupported => write!(f, "XorV1 proxy types are not supported"), + Self::InvalidPort(port) => write!(f, "Port {port} is not valid for remote endpoint"), + Self::EmptyXorKey => write!(f, "The key material for XOR obfuscation is empty"), + } + } +} + +impl std::error::Error for Error {} + +/// 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)] +enum ProxyType { + Plain, + XorV1, + XorV2, +} + +impl TryFrom<[u8; 2]> for ProxyType { + type Error = Error; + + fn try_from(bytes: [u8; 2]) -> Result { + match u16::from_le_bytes(bytes) { + 0x01 => Ok(Self::Plain), + 0x02 => Ok(Self::XorV1), + 0x03 => Ok(Self::XorV2), + unknown => Err(Error::UnknownProxyType(unknown)), + } + } +} + +pub trait Obfuscator: Send { + /// Applies obfuscation to a given buffer of bytes. Changes the data in place. + fn obfuscate(&mut self, buffer: &mut [u8]); +} + +/// Represents a Mullvad Encrypted DNS proxy configuration. Created by parsing +/// the config out of an IPv6 address resolved over DoH. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct ProxyConfig { + /// The remote address to connect to the proxy over. This is the address + /// on the internet where the proxy is listening. + pub addr: SocketAddrV4, + /// If the proxy requires some obfuscation of the data sent to/received from it, + /// it's represented by an obfuscation config here. + pub obfuscation: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum ObfuscationConfig { + XorV2(xor::XorKey), +} + +impl ObfuscationConfig { + /// Instantiate an obfuscator from the given obfuscation config. + pub fn create_obfuscator(&self) -> Box { + match self { + Self::XorV2(key) => Box::new(xor::XorObfuscator::new(*key)), + } + } +} + +impl TryFrom for ProxyConfig { + type Error = Error; + + fn try_from(ip: Ipv6Addr) -> Result { + let data = ip.octets(); + + let proxy_type_bytes = <[u8; 2]>::try_from(&data[2..4]).unwrap(); + let proxy_config_payload = <[u8; 12]>::try_from(&data[4..16]).unwrap(); + + let proxy_type = ProxyType::try_from(proxy_type_bytes)?; + + match proxy_type { + ProxyType::Plain => plain::parse_plain(proxy_config_payload), + ProxyType::XorV1 => Err(Error::XorV1Unsupported), + ProxyType::XorV2 => xor::parse_xor(proxy_config_payload), + } + } +} + +#[cfg(test)] +mod tests { + use std::net::Ipv6Addr; + + use super::{Error, ProxyConfig}; + + #[test] + fn wrong_proxy_type() { + let addr: Ipv6Addr = "ffff:2345::".parse().unwrap(); + match ProxyConfig::try_from(addr) { + Err(Error::UnknownProxyType(0x4523)) => (), + anything_else => panic!("Unexpected proxy config parse result: {anything_else:?}"), + } + } +} 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..7a467cca3352 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/config/plain.rs @@ -0,0 +1,79 @@ +use std::net::{Ipv4Addr, SocketAddrV4}; + +/// Parse a proxy config 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 [`super::ProxyType::Plain`]. Normally the +/// input to this function will come from the last 12 bytes of an IPv6 address. A plain +/// configuration interprets the following bytes from a given IPv6 address: +/// bytes 2-4 - u16le - proxy type - must be 0x01 +/// bytes 4-8 - [u8; 4] - 4 bytes representing the proxy IPv4 address +/// bytes 8-10 - 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. +pub fn parse_plain(data: [u8; 12]) -> Result { + let (ip_bytes, tail) = data.split_first_chunk::<4>().unwrap(); + let (port_bytes, _tail) = tail.split_first_chunk::<2>().unwrap(); + + let ip = Ipv4Addr::from(*ip_bytes); + let port = u16::from_le_bytes(*port_bytes); + if port == 0 { + return Err(super::Error::InvalidPort(0)); + } + let addr = SocketAddrV4::new(ip, port); + + Ok(super::ProxyConfig { + addr, + obfuscation: None, + }) +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv6Addr, SocketAddrV4}; + + use crate::config::{Error, ProxyConfig}; + + #[test] + fn parsing() { + struct Test { + input: Ipv6Addr, + expected: Result, + } + let tests = vec![ + Test { + input: "2001:100:7f00:1:3905::".parse::().unwrap(), + expected: Ok(ProxyConfig { + addr: "127.0.0.1:1337".parse::().unwrap(), + obfuscation: None, + }), + }, + Test { + input: "2001:100:c0a8:101:bb01::".parse::().unwrap(), + expected: Ok(ProxyConfig { + addr: "192.168.1.1:443".parse::().unwrap(), + obfuscation: None, + }), + }, + Test { + input: "2001:100:c0a8:101:bb01:404::".parse::().unwrap(), + expected: Ok(ProxyConfig { + addr: "192.168.1.1:443".parse::().unwrap(), + obfuscation: None, + }), + }, + Test { + input: "2001:100:c0a8:101:0000:404::".parse::().unwrap(), + expected: Err(Error::InvalidPort(0)), + }, + ]; + + for t in tests { + let parsed = ProxyConfig::try_from(t.input); + 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..b4dd2dbdf21b --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/config/xor.rs @@ -0,0 +1,204 @@ +use core::fmt; +use std::net::{Ipv4Addr, SocketAddrV4}; + +/// Parse a proxy config that XORs all traffic with the given key. +/// +/// A Xor configuration is represented by the proxy type `ProxyType::XorV2`. There used to be a `XorV1`, but it +/// is deprecated and should not be used. +/// +/// The following bytes of an IPv6 address are interpreted to derive a Xor configuration: +/// bytes 2-4 - u16le - proxy type - must be 0x03 +/// bytes 4-8 - [u8; 4] - 4 bytes representing the proxy IPv4 address +/// bytes 8-10 - u16le - port on which the proxy is listening +/// bytes 10-16 - [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). +pub fn parse_xor(data: [u8; 12]) -> Result { + let (ip_bytes, tail) = data.split_first_chunk::<4>().unwrap(); + let (port_bytes, key_bytes) = tail.split_first_chunk::<2>().unwrap(); + let key_bytes = <[u8; 6]>::try_from(key_bytes).unwrap(); + + let ip = Ipv4Addr::from(*ip_bytes); + let port = u16::from_le_bytes(*port_bytes); + if port == 0 { + return Err(super::Error::InvalidPort(port)); + } + let addr = SocketAddrV4::new(ip, port); + + let key = XorKey::try_from(key_bytes)?; + + Ok(super::ProxyConfig { + addr, + obfuscation: Some(super::ObfuscationConfig::XorV2(key)), + }) +} + +/// A bunch of bytes, representing a "key" Simply meaning a slice of bytes that the data +/// will be XORed with. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct XorKey { + data: [u8; 6], + len: usize, +} + +impl XorKey { + /// Return the XOR key material. Will always have at least length 1. + pub fn key_data(&self) -> &[u8] { + &self.data[0..self.len] + } +} + +impl fmt::Debug for XorKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "0x")?; + for byte in self.key_data() { + write!(f, "{byte:0>2x}")?; + } + Ok(()) + } +} + +impl TryFrom<[u8; 6]> for XorKey { + type Error = super::Error; + + fn try_from(mut key_bytes: [u8; 6]) -> Result { + let key_len = key_bytes + .iter() + .position(|b| *b == 0x00) + .unwrap_or(key_bytes.len()); + if key_len == 0 { + return Err(super::Error::EmptyXorKey); + } + + // Reset bytes after terminating null to zeros. + // Allows simpler implementations of Eq and Hash + key_bytes[key_len..].fill(0); + + Ok(Self { + data: key_bytes, + len: key_len, + }) + } +} + +#[derive(Debug)] +pub struct XorObfuscator { + key: XorKey, + key_index: usize, +} + +impl XorObfuscator { + pub fn new(key: XorKey) -> Self { + Self { key, key_index: 0 } + } +} + +impl super::Obfuscator for XorObfuscator { + fn obfuscate(&mut self, buffer: &mut [u8]) { + let key_data = self.key.key_data(); + for byte in buffer { + *byte ^= key_data[self.key_index % key_data.len()]; + self.key_index = (self.key_index + 1) % key_data.len(); + } + } +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv6Addr, SocketAddrV4}; + + use crate::config::xor::{XorKey, XorObfuscator}; + use crate::config::{Error, ObfuscationConfig, Obfuscator, ProxyConfig}; + + #[test] + fn xor_parsing() { + struct Test { + input: Ipv6Addr, + expected: Result, + } + let tests = vec![ + Test { + input: "2001:300:7f00:1:3905:0102:304:506" + .parse::() + .unwrap(), + expected: Ok(ProxyConfig { + addr: "127.0.0.1:1337".parse::().unwrap(), + obfuscation: Some(ObfuscationConfig::XorV2( + XorKey::try_from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]).unwrap(), + )), + }), + }, + Test { + input: "2001:300:7f00:1:3905:0100:304:506" + .parse::() + .unwrap(), + expected: Ok(ProxyConfig { + addr: "127.0.0.1:1337".parse::().unwrap(), + obfuscation: Some(ObfuscationConfig::XorV2( + XorKey::try_from([0x01, 0, 0, 0, 0, 0]).unwrap(), + )), + }), + }, + Test { + input: "2001:300:c0a8:101:bb01:ff04:204:0" + .parse::() + .unwrap(), + expected: Ok(ProxyConfig { + addr: "192.168.1.1:443".parse::().unwrap(), + obfuscation: Some(ObfuscationConfig::XorV2( + XorKey::try_from([0xff, 0x04, 0x02, 0x04, 0, 0]).unwrap(), + )), + }), + }, + ]; + + for t in tests { + let parsed = ProxyConfig::try_from(t.input); + assert_eq!(parsed, t.expected); + } + } + + #[test] + fn obfuscation() { + const INPUT: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let mut payload = INPUT.to_vec(); + + let xor_key = XorKey::try_from([0xff, 0x04, 0x02, 0x04, 0x00, 0x00]).unwrap(); + + let mut xor_obfuscator = XorObfuscator::new(xor_key); + let mut xor_deobfuscator = XorObfuscator::new(xor_key); + + xor_obfuscator.obfuscate(&mut payload); + + assert_eq!( + payload, + &[0xfe, 0x06, 0x01, 0x00, 0xfa, 0x02, 0x05, 0x0c, 0xf6, 0x0e] + ); + + xor_deobfuscator.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 old_xor_addr() { + match ProxyConfig::try_from( + "2001:200:7f00:1:3905:0102:304:506" + .parse::() + .unwrap(), + ) { + Err(Error::XorV1Unsupported) => (), + anything_else => panic!("Unexpected proxy config parse result: {anything_else:?}"), + } + } + + #[test] + fn xor_key_debug_fmt() { + let key = XorKey::try_from([0x01, 0xff, 0x31, 0x00, 0x00, 0x00]).unwrap(); + let key_str = format!("{key:?}"); + assert_eq!(key_str, "0x01ff31"); + } +} 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..2f3734aac7e4 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/config_resolver.rs @@ -0,0 +1,140 @@ +//! Resolve valid proxy configurations via DoH. +//! +use crate::config; +use core::fmt; +use hickory_resolver::{config::*, error::ResolveError, TokioAsyncResolver}; +use rustls::ClientConfig; +use std::{net::IpAddr, sync::Arc}; + +/// The port to connect to the DoH resolvers over. +const RESOLVER_PORT: u16 = 443; + +pub struct Nameserver { + pub name: String, + pub addr: Vec, +} + +#[derive(Debug)] +pub struct ResolutionError(ResolveError); + +impl fmt::Display for ResolutionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for ResolutionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.source() + } +} + +/// Returns a set of well known public DoH resolvers. A sane default in many cases. +pub fn default_resolvers() -> Vec { + vec![ + Nameserver { + name: "one.one.one.one".to_owned(), + addr: vec!["1.1.1.1".parse().unwrap(), "1.0.0.1".parse().unwrap()], + }, + Nameserver { + name: "dns.google".to_owned(), + addr: vec!["8.8.8.8".parse().unwrap(), "8.8.4.4".parse().unwrap()], + }, + Nameserver { + name: "dns.quad9.net".to_owned(), + addr: vec![ + "9.9.9.9".parse().unwrap(), + "149.112.112.112".parse().unwrap(), + ], + }, + ] +} + +/// Look up the `domain` towards the given `resolvers`, and try to deserialize all the returned +/// AAAA records into [`ProxyConfig`](config::ProxyConfig)s. +pub async fn resolve_configs( + resolvers: &[Nameserver], + domain: &str, +) -> Result, ResolutionError> { + let mut resolver_config = ResolverConfig::new(); + for resolver in resolvers.iter() { + let ns_config_group = NameServerConfigGroup::from_ips_https( + &resolver.addr, + RESOLVER_PORT, + resolver.name.clone(), + 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, ResolutionError> { + let resolver = TokioAsyncResolver::tokio(resolver_config, options); + let lookup = resolver + .ipv6_lookup(domain) + .await + .map_err(ResolutionError)?; + + let addrs = lookup.into_iter().map(|aaaa_record| aaaa_record.0); + + let mut proxy_configs = Vec::new(); + for addr in addrs { + match config::ProxyConfig::try_from(addr) { + Ok(proxy_config) => { + log::trace!("IPv6 {addr} parsed into proxy config: {proxy_config:?}"); + proxy_configs.push(proxy_config); + } + Err(e) => log::error!("IPv6 {addr} fails to parse to a proxy config: {e}"), + } + } + + Ok(proxy_configs) +} + +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_owned(), + }]; + + 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.rs b/mullvad-encrypted-dns-proxy/src/forwarder.rs new file mode 100644 index 000000000000..e8f366e16719 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/forwarder.rs @@ -0,0 +1,194 @@ +//! 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 if the proxy config says so. +/// +/// Obtain [`ProxyConfig`](crate::config::ProxyConfig)s with +/// [resolve_configs](crate::config_resolver::resolve_configs). +pub struct Forwarder { + read_obfuscator: Option>, + write_obfuscator: Option>, + server_connection: TcpStream, +} + +impl Forwarder { + /// Create a forwarder that will connect to a given proxy endpoint. + pub async fn connect(proxy_config: &crate::config::ProxyConfig) -> io::Result { + let server_connection = TcpStream::connect(proxy_config.addr).await?; + + let (read_obfuscator, write_obfuscator) = + if let Some(obfuscation_config) = &proxy_config.obfuscation { + ( + Some(obfuscation_config.create_obfuscator()), + Some(obfuscation_config.create_obfuscator()), + ) + } else { + (None, None) + }; + + Ok(Self { + read_obfuscator, + write_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(()) => { + if let Some(read_obfuscator) = &mut self.read_obfuscator { + 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(); + if let Some(write_obfuscator) = &mut self.write_obfuscator { + write_obfuscator.obfuscate(&mut owned_buf); + } + 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: Option>, + 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]; + + if let Some(obfuscator) = &mut obfuscator { + obfuscator.obfuscate(bytes_received); + } + sink.write_all(bytes_received).await?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, SocketAddrV4}; + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + }; + + use crate::config::{ObfuscationConfig, XorKey}; + + use super::Forwarder; + + // Constructs a server and a client, uses the Xor obfuscator to forward some bytes between to see + // the obfuscation works. + #[tokio::test] + async fn async_methods() { + const XOR_KEY: [u8; 6] = [0x01, 0x02, 0x03, 0x04, 0x00, 0x00]; + const LISTEN_IP: Ipv4Addr = Ipv4Addr::LOCALHOST; + + let server_listener = tokio::net::TcpListener::bind(SocketAddrV4::new(LISTEN_IP, 0)) + .await + .unwrap(); + let listen_port = server_listener.local_addr().unwrap().port(); + let listen_addr = SocketAddrV4::new(LISTEN_IP, listen_port); + + let xor_key = XorKey::try_from(XOR_KEY).unwrap(); + let obfuscation_config = ObfuscationConfig::XorV2(xor_key); + + let mut client_read_xor = obfuscation_config.create_obfuscator(); + let mut client_write_xor = obfuscation_config.create_obfuscator(); + + // 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: Some(obfuscation_config.create_obfuscator()), + write_obfuscator: Some(obfuscation_config.create_obfuscator()), + server_connection: client_conn, + }; + let mut buf = vec![0u8; 1024]; + while let Ok(bytes_read) = forwarder.read(&mut buf).await { + eprintln!("Forwarder read {bytes_read} bytes. Echoing them back"); + forwarder.write_all(&buf[..bytes_read]).await.unwrap(); + } + }); + + let mut client_connection = TcpStream::connect(listen_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..9734d3ea56a4 --- /dev/null +++ b/mullvad-encrypted-dns-proxy/src/lib.rs @@ -0,0 +1,13 @@ +//! Mullvad Encrypted DNS proxy is a custom protocol for reaching the Mullvad API over proxies, +//! with some amont of simple obfuscation applied. +//! +//! The proxy endpoints and what obfuscation they expect is fetched over DNS-over-HTTPS (DoH) +//! in AAAA records. The AAAA (IPv6) records are then decoded into a proxy config consisting +//! of a remote endpoint to connect to, and what obfuscation to use. +//! + +pub mod config; +pub mod config_resolver; +mod forwarder; + +pub use forwarder::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",