Skip to content

Commit

Permalink
Merge branch 'add-obfuscated-dns-proxy'
Browse files Browse the repository at this point in the history
  • Loading branch information
pinkisemils committed Sep 23, 2024
2 parents 7eca022 + f11e8b4 commit 5cadd01
Show file tree
Hide file tree
Showing 11 changed files with 846 additions and 0 deletions.
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",
"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

0 comments on commit 5cadd01

Please sign in to comment.