From 94c4114b38fa6e0ec7b5528222d489477ce453c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20F=C3=A4rnstrand?= Date: Thu, 19 Sep 2024 17:14:11 +0200 Subject: [PATCH] Rewrite parts of the Encrypted DNS proxy parsing and handling --- Cargo.lock | 1 - mullvad-encrypted-dns-proxy/Cargo.toml | 4 - .../examples/forwarder.rs | 20 +- mullvad-encrypted-dns-proxy/src/config/mod.rs | 176 +++++------ .../src/config/plain.rs | 136 ++++---- mullvad-encrypted-dns-proxy/src/config/xor.rs | 291 ++++++++++-------- .../src/config_resolver.rs | 22 +- .../src/forwarder/mod.rs | 142 +++++---- 8 files changed, 411 insertions(+), 381 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1611210b9ec9..2c76c1bd14d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2390,7 +2390,6 @@ dependencies = [ name = "mullvad-encrypted-dns-proxy" version = "0.0.0" dependencies = [ - "byteorder", "hickory-resolver", "log", "rustls", diff --git a/mullvad-encrypted-dns-proxy/Cargo.toml b/mullvad-encrypted-dns-proxy/Cargo.toml index 7b97bdfa627e..3d655125c48b 100644 --- a/mullvad-encrypted-dns-proxy/Cargo.toml +++ b/mullvad-encrypted-dns-proxy/Cargo.toml @@ -13,10 +13,6 @@ 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 index caf97a893fb5..c34cb6b9d73b 100644 --- a/mullvad-encrypted-dns-proxy/examples/forwarder.rs +++ b/mullvad-encrypted-dns-proxy/examples/forwarder.rs @@ -1,6 +1,6 @@ use std::env::args; -use mullvad_encrypted_dns_proxy::{config::Obfuscator, config_resolver, forwarder}; +use mullvad_encrypted_dns_proxy::{config_resolver, forwarder}; use tokio::net::TcpListener; /// This can be tested out by using curl: @@ -8,24 +8,30 @@ use tokio::net::TcpListener; /// where $addr and $port are the listening address of the proxy (bind_addr). #[tokio::main] async fn main() { - let mut configs = + let bind_addr = args().nth(1).unwrap_or("127.0.0.1:0".to_string()); + + let 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 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 { - let connected = crate::forwarder::Forwarder::connect(obfuscator.clone()) + let connected = crate::forwarder::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 index db967397d322..85bf27996c41 100644 --- a/mullvad-encrypted-dns-proxy/src/config/mod.rs +++ b/mullvad-encrypted-dns-proxy/src/config/mod.rs @@ -1,129 +1,117 @@ //! 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}; +use core::fmt; +use std::net::{Ipv6Addr, SocketAddrV4}; 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 use xor::XorKey; + +#[derive(Debug, Eq, PartialEq)] 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, + /// 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, } -/// 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); +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)] -#[repr(u16)] enum ProxyType { - Plain = 0x01, - XorV1 = 0x02, - XorV2 = 0x03, + Plain, + XorV1, + XorV2, } -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") - { +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(ErrorUnknownType(unknown)), + unknown => Err(Error::UnknownProxyType(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, +pub trait Obfuscator: Send { + /// Applies obfuscation to a given buffer of bytes. Changes the data in place. + fn obfuscate(&mut self, buffer: &mut [u8]); } -impl TryFrom> for AvailableProxies { - type Error = Error; +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct ProxyConfig { + pub addr: SocketAddrV4, + pub obfuscation: Option, +} - 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); - } +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum ObfuscationConfig { + XorV2(xor::XorKey), +} - Ok(proxies) +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)), + } } } -/// 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; +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), + } + } } -#[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?}"), +#[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 index fc750cdc17f4..f740cb6ff85f 100644 --- a/mullvad-encrypted-dns-proxy/src/config/plain.rs +++ b/mullvad-encrypted-dns-proxy/src/config/plain.rs @@ -1,100 +1,78 @@ -use byteorder::{LittleEndian, ReadBytesExt}; -use std::{ - io::{Cursor, Read}, - net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}, -}; +use std::net::{Ipv4Addr, SocketAddrV4}; -/// Obfuscator that does not obfuscate. It still can circumvent censorship since it is reaching our +/// 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 ProxyType::Plain (0x01). A plain +/// A plain configuration is represented by proxy type `ProxyType::Plain`. 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 +/// 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. -#[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(); +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(); - Ok(Self { - addr: SocketAddrV4::new(v4_addr, port), - }) + 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); -impl super::Obfuscator for Plain { - // can be a noop, since this configuration is just a port forward. - fn obfuscate(&mut self, _buffer: &mut [u8]) {} + Ok(super::ProxyConfig { + addr, + obfuscation: None, + }) +} - fn addr(&self) -> SocketAddrV4 { - self.addr - } +#[cfg(test)] +mod tests { + use std::net::{Ipv6Addr, SocketAddrV4}; - fn clone(&self) -> Box { - Box::new(Clone::clone(self)) - } -} + use crate::config::{Error, ProxyConfig}; -#[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] + 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: Plain { - addr: "192.168.1.1:443".parse::().unwrap(), + 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: Plain { - addr: "192.168.1.1:443".parse::().unwrap(), + 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 = Plain::try_from(t.input).unwrap(); - assert_eq!(parsed, t.expected); + 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 index 330a708f59f6..7cd0cb8b5f76 100644 --- a/mullvad-encrypted-dns-proxy/src/config/xor.rs +++ b/mullvad-encrypted-dns-proxy/src/config/xor.rs @@ -1,162 +1,201 @@ -use byteorder::{LittleEndian, ReadBytesExt}; -use std::{ - io::{Cursor, Read}, - net::{Ipv4Addr, Ipv6Addr, SocketAddrV4}, -}; +use core::fmt; +use std::net::{Ipv4Addr, SocketAddrV4}; -use crate::config::Obfuscator; - -/// An obfuscator that XORs all traffic with the given key. +/// 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. /// -/// 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 +/// 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 +/// 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, -} +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); -#[derive(Debug)] -pub enum Error { - EmptyXorKey, - UnexpectedType(u16), + let key = XorKey::try_from(key_bytes)?; + + Ok(super::ProxyConfig { + addr, + obfuscation: Some(super::ObfuscationConfig::XorV2(key)), + }) } -impl TryFrom for Xor { - type Error = Error; +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct XorKey { + data: [u8; 6], + len: usize, +} - fn try_from(ip: Ipv6Addr) -> Result { - let mut cursor = Cursor::new(ip.octets()); +impl XorKey { + pub fn key_data(&self) -> &[u8] { + &self.data[0..self.len] + } +} - 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)); +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(()) + } +} - 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(); +impl TryFrom<[u8; 6]> for XorKey { + type Error = super::Error; - 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); + 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 { - addr: SocketAddrV4::new(v4_addr, port), - xor_key, - key_index: 0, + data: key_bytes, + len: key_len, }) } } -impl Obfuscator for Xor { - fn addr(&self) -> SocketAddrV4 { - self.addr +#[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]) { - 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(); + 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(); } } - - 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, +#[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: Xor { - addr: "127.0.0.1:1337".parse::().unwrap(), - xor_key: vec![0x01], - key_index: 0, + 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: Xor { - addr: "192.168.1.1:443".parse::().unwrap(), - xor_key: vec![0xff, 0x04, 0x02, 0x04], - key_index: 0, + 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 = Xor::try_from(t.input).unwrap(); - assert_eq!(parsed, t.expected); + for t in tests { + let parsed = ProxyConfig::try_from(t.input); + 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()); -} + #[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(); -// 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(); + 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 index 2b0ede61ae01..44b934c04097 100644 --- a/mullvad-encrypted-dns-proxy/src/config_resolver.rs +++ b/mullvad-encrypted-dns-proxy/src/config_resolver.rs @@ -39,7 +39,7 @@ pub fn default_resolvers() -> Vec { pub async fn resolve_configs( resolvers: Vec, domain: &str, -) -> Result { +) -> Result, Error> { let mut resolver_config = ResolverConfig::new(); for resolver in resolvers.into_iter() { let ns_config_group = @@ -59,19 +59,27 @@ pub async fn resolve_config_with_resolverconfig( resolver_config: ResolverConfig, options: ResolverOpts, domain: &str, -) -> Result { +) -> Result, Error> { 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::>(); + let addrs = lookup.into_iter().map(|aaaa_record| aaaa_record.0); - config::AvailableProxies::try_from(addrs).map_err(Error::ParsingError) + 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 { diff --git a/mullvad-encrypted-dns-proxy/src/forwarder/mod.rs b/mullvad-encrypted-dns-proxy/src/forwarder/mod.rs index 7ae905525ad6..add86ba40a2d 100644 --- a/mullvad-encrypted-dns-proxy/src/forwarder/mod.rs +++ b/mullvad-encrypted-dns-proxy/src/forwarder/mod.rs @@ -1,4 +1,5 @@ //! Forward TCP traffic over various proxy configurations. + use std::{ io, task::{ready, Poll}, @@ -13,19 +14,29 @@ use crate::config::Obfuscator; /// Forwards local traffic to a proxy endpoint, obfuscating it. pub struct Forwarder { - read_obfuscator: Box, - write_obfuscator: Box, + 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(obfuscator: Box) -> io::Result { - let server_connection = TcpStream::connect(obfuscator.addr()).await?; + 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: obfuscator.clone(), - write_obfuscator: obfuscator, + read_obfuscator, + write_obfuscator, server_connection, }) } @@ -52,7 +63,9 @@ impl tokio::io::AsyncRead for Forwarder { match ready!(socket.poll_read(cx, buf)) { // in this case, we can read and deobfuscate. Ok(()) => { - self.read_obfuscator.obfuscate(buf.filled_mut()); + if let Some(read_obfuscator) = &mut self.read_obfuscator { + read_obfuscator.obfuscate(buf.filled_mut()); + } Poll::Ready(Ok(())) } Err(err) => Poll::Ready(Err(err)), @@ -72,7 +85,9 @@ impl tokio::io::AsyncWrite for Forwarder { }; let mut owned_buf = buf.to_vec(); - self.write_obfuscator.obfuscate(owned_buf.as_mut_slice()); + 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) } @@ -93,7 +108,7 @@ impl tokio::io::AsyncWrite for Forwarder { } async fn forward( - mut obfuscator: Box, + mut obfuscator: Option>, mut source: impl AsyncRead + Unpin, mut sink: impl AsyncWrite + Unpin, ) -> io::Result<()> { @@ -105,71 +120,72 @@ async fn forward( } let bytes_received = &mut buf[..n_bytes_read]; - obfuscator.obfuscate(bytes_received); + if let Some(obfuscator) = &mut obfuscator { + 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; +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, SocketAddrV4}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, }; - let server_listener = - tokio::net::TcpListener::bind("127.0.0.1:0".parse::().unwrap()) + + 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 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 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); } - }); - - 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); } }