-
Notifications
You must be signed in to change notification settings - Fork 349
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'add-obfuscated-dns-proxy'
- Loading branch information
Showing
11 changed files
with
846 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:?}"), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.