Skip to content

Commit

Permalink
Add mullvad-obfuscated-dns-proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
pinkisemils authored and faern committed Sep 20, 2024
1 parent c268dda commit fcc75e9
Show file tree
Hide file tree
Showing 11 changed files with 769 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
22 changes: 22 additions & 0 deletions mullvad-encrypted-dns-proxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[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 }
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" ]}
33 changes: 33 additions & 0 deletions mullvad-encrypted-dns-proxy/examples/forwarder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use std::env::args;

use mullvad_encrypted_dns_proxy::{config::Obfuscator, config_resolver, forwarder};
use tokio::net::TcpListener;

/// This can be tested out by using curl:
/// `curl https://api.mullvad.net:$port/api/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() {
let mut 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<dyn Obfuscator> = Box::new(obfuscator);
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())
.await
.expect("failed to connect to obfuscator");
let _ = connected.forward(client_conn).await;
}
}
129 changes: 129 additions & 0 deletions mullvad-encrypted-dns-proxy/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! 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};

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 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,
}

/// 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);

/// 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,
}

impl TryFrom<Ipv6Addr> 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<Self, Self::Error> {
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::<LittleEndian>()
.expect("IPv6 must have at least 16 bytes")
{
0x01 => Ok(Self::Plain),
0x02 => Ok(Self::XorV1),
0x03 => Ok(Self::XorV2),
unknown => Err(ErrorUnknownType(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<Plain>,
/// Xor proxies xor a pre-shared key with all the traffic.
pub xor: Vec<Xor>,
}

impl TryFrom<Vec<Ipv6Addr>> for AvailableProxies {
type Error = Error;

fn try_from(ips: Vec<Ipv6Addr>) -> Result<Self, Self::Error> {
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);
}

Ok(proxies)
}
}

/// 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<dyn Obfuscator>;
}

#[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?}"),
}
}
100 changes: 100 additions & 0 deletions mullvad-encrypted-dns-proxy/src/config/plain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use byteorder::{LittleEndian, ReadBytesExt};
use std::{
io::{Cursor, Read},
net::{Ipv4Addr, Ipv6Addr, SocketAddrV4},
};

/// Obfuscator 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
/// 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
///
/// 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<Ipv6Addr> for Plain {
type Error = Error;

fn try_from(ip: Ipv6Addr) -> Result<Self, Self::Error> {
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::<LittleEndian>().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::<LittleEndian>().unwrap();

Ok(Self {
addr: SocketAddrV4::new(v4_addr, 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]) {}

fn addr(&self) -> SocketAddrV4 {
self.addr
}

fn clone(&self) -> Box<dyn super::Obfuscator> {
Box::new(Clone::clone(self))
}
}

#[test]
fn test_parsing() {
struct Test {
input: Ipv6Addr,
expected: Plain,
}
let tests = vec![
Test {
input: "2001:100:7f00:1:3905::".parse::<Ipv6Addr>().unwrap(),
expected: Plain {
addr: "127.0.0.1:1337".parse::<SocketAddrV4>().unwrap(),
},
},
Test {
input: "2001:100:c0a8:101:bb01::".parse::<Ipv6Addr>().unwrap(),
expected: Plain {
addr: "192.168.1.1:443".parse::<SocketAddrV4>().unwrap(),
},
},
Test {
input: "2001:100:c0a8:101:bb01:404::".parse::<Ipv6Addr>().unwrap(),
expected: Plain {
addr: "192.168.1.1:443".parse::<SocketAddrV4>().unwrap(),
},
},
];

for t in tests {
let parsed = Plain::try_from(t.input).unwrap();
assert_eq!(parsed, t.expected);
}
}
Loading

0 comments on commit fcc75e9

Please sign in to comment.