summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2024-09-23 10:09:38 +0200
committerEmīls <emils@mullvad.net>2024-09-23 10:09:38 +0200
commit5cadd01fe2b9b1b24942328002bafa2c565522c2 (patch)
tree262c2d202842c4c3785e03955e729a59a4da25f5
parent7eca0227057a688c8a0372e43e47461fddeb1b6c (diff)
parentf11e8b493891ad0af760d20deae3ae3e47df4849 (diff)
downloadmullvadvpn-5cadd01fe2b9b1b24942328002bafa2c565522c2.tar.xz
mullvadvpn-5cadd01fe2b9b1b24942328002bafa2c565522c2.zip
Merge branch 'add-obfuscated-dns-proxy'
-rw-r--r--Cargo.lock27
-rw-r--r--Cargo.toml1
-rw-r--r--mullvad-encrypted-dns-proxy/Cargo.toml21
-rw-r--r--mullvad-encrypted-dns-proxy/examples/forwarder.rs42
-rw-r--r--mullvad-encrypted-dns-proxy/src/config/mod.rs124
-rw-r--r--mullvad-encrypted-dns-proxy/src/config/plain.rs79
-rw-r--r--mullvad-encrypted-dns-proxy/src/config/xor.rs204
-rw-r--r--mullvad-encrypted-dns-proxy/src/config_resolver.rs140
-rw-r--r--mullvad-encrypted-dns-proxy/src/forwarder.rs194
-rw-r--r--mullvad-encrypted-dns-proxy/src/lib.rs13
-rw-r--r--mullvad-ios/Cargo.toml1
11 files changed, 846 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c0c8329eed..ed52bbb2f8 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",
]
@@ -2379,6 +2387,18 @@ dependencies = [
]
[[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"
dependencies = [
@@ -2404,6 +2424,7 @@ dependencies = [
"cbindgen",
"libc",
"log",
+ "mullvad-encrypted-dns-proxy",
"oslog",
"shadowsocks-service",
"talpid-tunnel-config-client",
@@ -4854,6 +4875,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 728799f739..0b674d289b 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 0000000000..4f101e3ed0
--- /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 0000000000..00302886a2
--- /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 0000000000..cd93d2989e
--- /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<Self, Self::Error> {
+ 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<ObfuscationConfig>,
+}
+
+#[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<dyn Obfuscator> {
+ match self {
+ Self::XorV2(key) => Box::new(xor::XorObfuscator::new(*key)),
+ }
+ }
+}
+
+impl TryFrom<Ipv6Addr> for ProxyConfig {
+ type Error = Error;
+
+ fn try_from(ip: Ipv6Addr) -> Result<Self, Self::Error> {
+ 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 0000000000..7a467cca33
--- /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<super::ProxyConfig, super::Error> {
+ 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<ProxyConfig, Error>,
+ }
+ let tests = vec![
+ Test {
+ input: "2001:100:7f00:1:3905::".parse::<Ipv6Addr>().unwrap(),
+ expected: Ok(ProxyConfig {
+ addr: "127.0.0.1:1337".parse::<SocketAddrV4>().unwrap(),
+ obfuscation: None,
+ }),
+ },
+ Test {
+ input: "2001:100:c0a8:101:bb01::".parse::<Ipv6Addr>().unwrap(),
+ expected: Ok(ProxyConfig {
+ addr: "192.168.1.1:443".parse::<SocketAddrV4>().unwrap(),
+ obfuscation: None,
+ }),
+ },
+ Test {
+ input: "2001:100:c0a8:101:bb01:404::".parse::<Ipv6Addr>().unwrap(),
+ expected: Ok(ProxyConfig {
+ addr: "192.168.1.1:443".parse::<SocketAddrV4>().unwrap(),
+ obfuscation: None,
+ }),
+ },
+ Test {
+ input: "2001:100:c0a8:101:0000:404::".parse::<Ipv6Addr>().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 0000000000..b4dd2dbdf2
--- /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<super::ProxyConfig, super::Error> {
+ 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<Self, Self::Error> {
+ 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<ProxyConfig, Error>,
+ }
+ let tests = vec![
+ Test {
+ input: "2001:300:7f00:1:3905:0102:304:506"
+ .parse::<Ipv6Addr>()
+ .unwrap(),
+ expected: Ok(ProxyConfig {
+ addr: "127.0.0.1:1337".parse::<SocketAddrV4>().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::<Ipv6Addr>()
+ .unwrap(),
+ expected: Ok(ProxyConfig {
+ addr: "127.0.0.1:1337".parse::<SocketAddrV4>().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::<Ipv6Addr>()
+ .unwrap(),
+ expected: Ok(ProxyConfig {
+ addr: "192.168.1.1:443".parse::<SocketAddrV4>().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::<Ipv6Addr>()
+ .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 0000000000..2f3734aac7
--- /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<IpAddr>,
+}
+
+#[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<Nameserver> {
+ 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<Vec<config::ProxyConfig>, 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<Vec<config::ProxyConfig>, 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 0000000000..e8f366e167
--- /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<Box<dyn Obfuscator>>,
+ write_obfuscator: Option<Box<dyn Obfuscator>>,
+ 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<Self> {
+ 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<std::io::Result<()>> {
+ 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<Result<usize, io::Error>> {
+ 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<Result<(), io::Error>> {
+ 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<Result<(), io::Error>> {
+ std::pin::pin!(&mut self.server_connection).poll_shutdown(cx)
+ }
+}
+
+async fn forward(
+ mut obfuscator: Option<Box<dyn Obfuscator>>,
+ 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::<Vec<u8>>();
+ 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 0000000000..9734d3ea56
--- /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 6e0fedb8df..83d3af7447 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",