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

Support Oblivious Proxy function in ODoH #64

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ readme = "README.md"
[features]
default = ["tls"]
tls = ["libdoh/tls"]
odoh-proxy = ["libdoh/odoh-proxy"]

[dependencies]
libdoh = { path = "src/libdoh", version = "0.9.0", default-features = false }
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,20 @@ cargo install doh-proxy
cargo install doh-proxy --no-default-features
```

* With Oblivious DoH Proxy function (**for testing pupose**):

```sh
cargo install doh-proxy --features=doh-proxy
```

## Usage

```text
USAGE:
doh-proxy [FLAGS] [OPTIONS]

FLAGS:
-O, --allow-odoh-post Allow POST queries over ODoH even if they have been disabed for DoH
-O, --allow-odoh-post Allow POST queries over ODoH even if they have been disabled for DoH
-K, --disable-keepalive Disable keepalive
-P, --disable-post Disable POST queries
-h, --help Prints help information
Expand All @@ -48,6 +54,7 @@ OPTIONS:
-C, --max-concurrent <max_concurrent> Maximum number of concurrent requests per client [default: 16]
-X, --max-ttl <max_ttl> Maximum TTL, in seconds [default: 604800]
-T, --min-ttl <min_ttl> Minimum TTL, in seconds [default: 10]
-q, --odoh-proxy-path <odoh_proxy_path> ODoH proxy URI path [default: /proxy]
-p, --path <path> URI path [default: /dns-query]
-g, --public-address <public_address> External IP address DoH clients will connect to
-j, --public-port <public_port> External port DoH clients will connect to, if not 443
Expand Down Expand Up @@ -113,11 +120,13 @@ Unless the front-end is a CDN, an ideal setup is to use `doh-proxy` behind `Encr

Oblivious DoH is similar to Anonymized DNSCrypt, but for DoH. It requires relays, but also upstream DoH servers that support the protocol.

This proxy supports ODoH termination (not relaying) out of the box.
This proxy supports ODoH termination out of the box.

However, ephemeral keys are currently only stored in memory. In a load-balanced configuration, sticky sessions must be used.

Currently available ODoH relays only use `POST` queries.
This also also provides ODoH relaying (Oblivious Proxy) of naive implementation, which is **for testing purposes only**. Please do not deploy the relaying function AS-IS. You need to carefully consider the performance and security issues when you deploy ODoH relays. Further, the relaying protocol is not fully fixed yet in the IETF draft.

As currently available ODoH relays only use `POST` queries, this proxy accepts and issues `POST` queries both in ODoH target and relay functions.
So, `POST` queries have been disabled for regular DoH queries, accepting them is required to be compatible with ODoH relays.

This can be achieved with the `--allow-odoh-post` command-line switch.
Expand Down
35 changes: 33 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ pub fn parse_opts(globals: &mut Globals) {
Arg::with_name("allow_odoh_post")
.short("O")
.long("allow-odoh-post")
.help("Allow POST queries over ODoH even if they have been disabed for DoH"),
.help("Allow POST queries over ODoH even if they have been disabled for DoH"),
);

#[cfg(feature = "tls")]
Expand All @@ -162,6 +162,17 @@ pub fn parse_opts(globals: &mut Globals) {
.help("Path to the PEM-encoded secret keys (only required for built-in TLS)"),
);

#[cfg(feature = "odoh-proxy")]
let options = options
.arg(
Arg::with_name("odoh_proxy_path")
.short("q")
.long("odoh-proxy-path")
.takes_value(true)
.default_value(ODOH_PROXY_PATH)
.help("ODoH proxy URI path"),
);

let matches = options.get_matches();
globals.listen_address = matches.value_of("listen_address").unwrap().parse().unwrap();

Expand Down Expand Up @@ -207,6 +218,15 @@ pub fn parse_opts(globals: &mut Globals) {
.or_else(|| globals.tls_cert_path.clone());
}

#[cfg(feature = "odoh-proxy")]
{
globals.odoh_proxy_path = matches.value_of("odoh_proxy_path").unwrap().to_string();
if !globals.odoh_proxy_path.starts_with('/') {
globals.odoh_proxy_path = format!("/{}", globals.odoh_proxy_path);
}
globals.odoh_proxy = libdoh::odoh_proxy::ODoHProxy::new(globals.timeout).unwrap();
}

if let Some(hostname) = matches.value_of("hostname") {
let mut builder =
dnsstamps::DoHBuilder::new(hostname.to_string(), globals.path.to_string());
Expand All @@ -230,11 +250,22 @@ pub fn parse_opts(globals: &mut Globals) {
builder = builder.with_port(public_port);
}
println!(
"Test DNS stamp to reach [{}] over Oblivious DoH: [{}]\n",
"Test DNS stamp to reach [{}] over Oblivious DoH Target: [{}]\n",
hostname,
builder.serialize().unwrap()
);

#[cfg(feature = "odoh-proxy")]
{
let builder =
dnsstamps::ODoHRelayBuilder::new(hostname.to_string(), globals.odoh_proxy_path.to_string());
println!(
"Test DNS stamp to reach [{}] over Oblivious DoH Proxy: [{}]\n",
hostname,
builder.serialize().unwrap()
);
}

println!("Check out https://dnscrypt.info/stamps/ to compute the actual stamps.\n")
} else {
println!("Please provide a fully qualified hostname (-H <hostname> command-line option) to get test DNS stamps for your server.\n");
Expand Down
2 changes: 2 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ pub const LISTEN_ADDRESS: &str = "127.0.0.1:3000";
pub const MAX_CLIENTS: usize = 512;
pub const MAX_CONCURRENT_STREAMS: u32 = 16;
pub const PATH: &str = "/dns-query";
#[cfg(feature = "odoh-proxy")]
pub const ODOH_PROXY_PATH: &str = "/proxy";
pub const ODOH_CONFIGS_PATH: &str = "/.well-known/odohconfigs";
pub const SERVER_ADDRESS: &str = "9.9.9.9:53";
pub const TIMEOUT_SEC: u64 = 10;
Expand Down
3 changes: 3 additions & 0 deletions src/libdoh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ edition = "2018"
[features]
default = ["tls"]
tls = ["tokio-rustls"]
odoh-proxy = ["reqwest", "urlencoding"]

[dependencies]
anyhow = "1.0.44"
Expand All @@ -25,9 +26,11 @@ hpke = "0.5.1"
hyper = { version = "0.14.14", default-features = false, features = ["server", "http1", "http2", "stream"] }
odoh-rs = "1.0.0-alpha.1"
rand = "0.8.4"
reqwest = { version = "0.11.4", features = ["trust-dns"], optional = true}
tokio = { version = "1.13.0", features = ["net", "rt-multi-thread", "parking_lot", "time", "sync"] }
tokio-rustls = { version = "0.23.0", features = ["early-data"], optional = true }
rustls-pemfile = "0.2.1"
urlencoding = { version = "2.1.0", optional = true }

[profile.release]
codegen-units = 1
Expand Down
4 changes: 4 additions & 0 deletions src/libdoh/src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
pub const DNS_QUERY_PARAM: &str = "dns";
#[cfg(feature = "odoh-proxy")]
pub const ODOH_TARGET_HOST_QUERY_PARAM: &str = "targethost";
#[cfg(feature = "odoh-proxy")]
pub const ODOH_TARGET_PATH_QUERY_PARAM: &str = "targetpath";
pub const MAX_DNS_QUESTION_LEN: usize = 512;
pub const MAX_DNS_RESPONSE_LEN: usize = 4096;
pub const MIN_DNS_PACKET_LEN: usize = 17;
Expand Down
8 changes: 8 additions & 0 deletions src/libdoh/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use hyper::StatusCode;
use std::io;
#[cfg(feature = "odoh-proxy")]
use reqwest;

#[derive(Debug)]
pub enum DoHError {
Expand All @@ -10,6 +12,8 @@ pub enum DoHError {
UpstreamTimeout,
StaleKey,
Hyper(hyper::Error),
#[cfg(feature = "odoh-proxy")]
Reqwest(reqwest::Error),
Io(io::Error),
ODoHConfigError(anyhow::Error),
TooManyTcpSessions,
Expand All @@ -27,6 +31,8 @@ impl std::fmt::Display for DoHError {
DoHError::UpstreamTimeout => write!(fmt, "Upstream timeout"),
DoHError::StaleKey => write!(fmt, "Stale key material"),
DoHError::Hyper(e) => write!(fmt, "HTTP error: {}", e),
#[cfg(feature = "odoh-proxy")]
DoHError::Reqwest(e) => write!(fmt, "HTTP Proxy error: {}", e),
DoHError::Io(e) => write!(fmt, "IO error: {}", e),
DoHError::ODoHConfigError(e) => write!(fmt, "ODoH config error: {}", e),
DoHError::TooManyTcpSessions => write!(fmt, "Too many TCP sessions"),
Expand All @@ -44,6 +50,8 @@ impl From<DoHError> for StatusCode {
DoHError::UpstreamTimeout => StatusCode::BAD_GATEWAY,
DoHError::StaleKey => StatusCode::UNAUTHORIZED,
DoHError::Hyper(_) => StatusCode::SERVICE_UNAVAILABLE,
#[cfg(feature = "odoh-proxy")]
DoHError::Reqwest(_) => StatusCode::SERVICE_UNAVAILABLE,
DoHError::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
DoHError::ODoHConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR,
DoHError::TooManyTcpSessions => StatusCode::SERVICE_UNAVAILABLE,
Expand Down
8 changes: 8 additions & 0 deletions src/libdoh/src/globals.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::odoh::ODoHRotator;
#[cfg(feature = "odoh-proxy")]
use crate::odoh_proxy::ODoHProxy;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
Expand Down Expand Up @@ -33,6 +35,12 @@ pub struct Globals {
pub odoh_configs_path: String,
pub odoh_rotator: Arc<ODoHRotator>,

#[cfg(feature = "odoh-proxy")]
pub odoh_proxy_path: String,

#[cfg(feature = "odoh-proxy")]
pub odoh_proxy: ODoHProxy,

pub runtime_handle: runtime::Handle,
}

Expand Down
95 changes: 92 additions & 3 deletions src/libdoh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pub mod dns;
mod errors;
mod globals;
pub mod odoh;
#[cfg(feature = "odoh-proxy")]
pub mod odoh_proxy;
#[cfg(feature = "tls")]
mod tls;

Expand Down Expand Up @@ -109,7 +111,27 @@ impl hyper::service::Service<http::Request<Body>> for DoH {
_ => Box::pin(async { http_error(StatusCode::METHOD_NOT_ALLOWED) }),
}
} else {
Box::pin(async { http_error(StatusCode::NOT_FOUND) })
#[cfg(not(feature = "odoh-proxy"))]
{
Box::pin(async { http_error(StatusCode::NOT_FOUND) })
}
#[cfg(feature = "odoh-proxy")]
{
if req.uri().path() == globals.odoh_proxy_path {
// Draft: https://datatracker.ietf.org/doc/html/draft-pauly-dprive-oblivious-doh-06
// Golang impl.: https://github.com/cloudflare/odoh-server-go
// Based on the draft and Golang implementation, only post method is allowed.
match *req.method() {
Method::POST => {
Box::pin(async move { self_inner.serve_odoh_proxy_post(req).await })
}
_ => Box::pin(async { http_error(StatusCode::METHOD_NOT_ALLOWED) }),
}
}
else {
Box::pin(async { http_error(StatusCode::NOT_FOUND) })
}
}
}
}
}
Expand Down Expand Up @@ -228,6 +250,66 @@ impl DoH {
self.serve_odoh(encrypted_query).await
}

#[cfg(feature = "odoh-proxy")]
async fn serve_odoh_proxy(
&self,
encrypted_query: Vec<u8>,
target_uri: &str,
) -> Result<Response<Body>, http::Error> {
let encrypted_response = match self
.globals
.odoh_proxy
.forward_to_target(&encrypted_query, target_uri)
.await
{
Ok(resp) => self.build_response(resp, 0u32, DoHType::Oblivious.as_str(), true),
Err(e) => return http_error(e),
};

match encrypted_response {
Ok(resp) => Ok(resp),
Err(e) => http_error(StatusCode::from(e)),
}
}

#[cfg(feature = "odoh-proxy")]
async fn serve_odoh_proxy_post(
&self,
req: Request<Body>,
) -> Result<Response<Body>, http::Error> {
if self.globals.disable_post && !self.globals.allow_odoh_post {
return http_error(StatusCode::METHOD_NOT_ALLOWED);
}
// Draft: https://datatracker.ietf.org/doc/html/draft-pauly-dprive-oblivious-doh-06
// Golang impl.: https://github.com/cloudflare/odoh-server-go
// The following follows the Golang implementation, which is different from the draft.
// In the draft, single endpoint, i.e., /dns-query, can accept proxy and target messages,
// and works as a proxy only when '?targethost' and '?tagetpath' exist in given uri query.
// However, in Golang implementation, proxy and target endpoints are separated.
match Self::parse_content_type(&req) {
Ok(DoHType::Oblivious) => {
let http_query = req.uri().query().unwrap_or("");
let target_uri = match odoh_proxy::target_uri_from_query_string(http_query) {
Some(uri) => uri,
_ => return http_error(StatusCode::BAD_REQUEST),
};
let encrypted_query = match self.read_body(req.into_body()).await {
Ok(q) => {
if q.len() == 0 {
return http_error(StatusCode::BAD_REQUEST);
}
q
},
Err(e) => return http_error(StatusCode::from(e)),
};

self.serve_odoh_proxy(encrypted_query, &target_uri).await
},
Ok(_) => http_error(StatusCode::UNSUPPORTED_MEDIA_TYPE),
Err(err_response) => Ok(err_response)
}
}

async fn serve_odoh_configs(&self) -> Result<Response<Body>, http::Error> {
let odoh_public_key = (*self.globals.odoh_rotator).clone().current_public_key();
let configs = (*odoh_public_key).clone().into_config();
Expand Down Expand Up @@ -483,6 +565,9 @@ impl DoH {
.map_err(DoHError::Io)?;
let path = &self.globals.path;

#[cfg(feature = "odoh-proxy")]
let odoh_proxy_path = &self.globals.odoh_proxy_path;

let tls_enabled: bool;
#[cfg(not(feature = "tls"))]
{
Expand All @@ -494,9 +579,13 @@ impl DoH {
self.globals.tls_cert_path.is_some() && self.globals.tls_cert_key_path.is_some();
}
if tls_enabled {
println!("Listening on https://{}{}", listen_address, path);
println!("ODoH/DoH Server: Listening on https://{}{}", listen_address, path);
#[cfg(feature = "odoh-proxy")]
println!("ODoH Proxy : Listening on https://{}{}", listen_address, odoh_proxy_path);
} else {
println!("Listening on http://{}{}", listen_address, path);
println!("ODoH/DoH Server: Listening on http://{}{}", listen_address, path);
#[cfg(feature = "odoh-proxy")]
println!("ODoH Proxy : Listening on http://{}{}", listen_address, odoh_proxy_path);
}

let mut server = Http::new();
Expand Down
Loading