Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mullvad-encrypted-dns-proxy crate for API obfuscation #6768

Merged
merged 1 commit into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ members = [
"mullvad-jni",
"mullvad-management-interface",
"mullvad-nsis",
"mullvad-encrypted-dns-proxy",
"mullvad-paths",
Serock3 marked this conversation as resolved.
Show resolved Hide resolved
"mullvad-problem-report",
"mullvad-relay-selector",
Expand Down
21 changes: 21 additions & 0 deletions mullvad-encrypted-dns-proxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
42 changes: 42 additions & 0 deletions mullvad-encrypted-dns-proxy/examples/forwarder.rs
Original file line number Diff line number Diff line change
@@ -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;
}
}
124 changes: 124 additions & 0 deletions mullvad-encrypted-dns-proxy/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -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:?}"),
}
}
}
79 changes: 79 additions & 0 deletions mullvad-encrypted-dns-proxy/src/config/plain.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading
Loading