-
Notifications
You must be signed in to change notification settings - Fork 339
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c268dda
commit fcc75e9
Showing
11 changed files
with
769 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,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" ]} |
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,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; | ||
} | ||
} |
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,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?}"), | ||
} | ||
} |
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,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); | ||
} | ||
} |
Oops, something went wrong.