Skip to content

Commit

Permalink
feat: odoh config watch service
Browse files Browse the repository at this point in the history
  • Loading branch information
junkurihara committed Oct 27, 2023
1 parent 15e48c7 commit 8a87d35
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 13 deletions.
1 change: 0 additions & 1 deletion dap-bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ fn main() {
runtime_builder.enable_all();
runtime_builder.thread_name("doh-auth-proxy");
let runtime = runtime_builder.build().unwrap();
println!("ok");

runtime.block_on(async {
// Initially load options
Expand Down
4 changes: 4 additions & 0 deletions dap-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ serde = { version = "1.0.189", features = ["derive"] }
itertools = "0.11.0"
rustc-hash = "1.1.0"

# odoh
odoh-rs = { git = "https://github.com/junkurihara/odoh-rs.git" }
bytes = "1.5.0"

# network
socket2 = "0.5.5"

Expand Down
8 changes: 7 additions & 1 deletion dap-lib/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ pub const MAX_CACHE_SIZE: usize = 16384;
// Constant Values for Proxy //
///////////////////////////////
// Cannot override below by config.toml
pub const ODOH_CONFIG_PATH: &str = ".well-known/odohconfigs"; // client

// ODoH

/// ODoH config path
pub const ODOH_CONFIG_PATH: &str = ".well-known/odohconfigs";
/// ODoH config is retrieved every 3600 secs
pub const ODOH_CONFIG_WATCH_DELAY: i64 = 60;

// Authentication

Expand Down
36 changes: 29 additions & 7 deletions dap-lib/src/doh_client/doh_client_main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use super::{
odoh_config_store::ODoHConfigStore,
path_manage::{self, DoHPathManager},
};
use crate::{
auth::Authenticator,
error::*,
Expand All @@ -10,8 +14,6 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use url::Url;

use super::path_manage::DoHPathManager;

/// DoH, ODoH, MODoH client
pub struct DoHClient {
/// http client to make doh query
Expand All @@ -20,23 +22,43 @@ pub struct DoHClient {
auth_client: Option<Arc<Authenticator>>,
/// path candidates with health flags
path_manager: Arc<DoHPathManager>,
// TODO: odoh config
// odoh config store
odoh_configs: Option<Arc<ODoHConfigStore>>,
}

impl DoHClient {
/// Create a new DoH client
pub fn new(
pub async fn new(
globals: Arc<Globals>,
http_client: Arc<RwLock<HttpClientInner>>,
auth_client: Option<Arc<Authenticator>>,
) -> Result<Self> {
// TODO: 1. build all path candidates from globals
// TODO: 2. spawn odoh config service
// 1. build all path candidates from globals
let path_manager = Arc::new(DoHPathManager::new(&globals)?);

// spawn odoh config service if odoh or modoh are enabled
let odoh_configs = match &globals.proxy_config.nexthop_relay_config {
Some(nexthop_relay_config) => {
if nexthop_relay_config.odoh_relay_urls.is_empty() {
return Err(DapError::ODoHNoRelayUrl);
}
let odoh_configs = Arc::new(ODoHConfigStore::new(http_client.clone(), &path_manager.targets()).await?);
let odoh_config_clone = odoh_configs.clone();
let term_notify = globals.term_notify.clone();
globals
.runtime_handle
.spawn(async move { odoh_config_clone.start_service(term_notify).await });
Some(odoh_configs)
}
None => None,
};

// TODO: 3. spawn healthcheck for every possible path? too many?
Ok(Self {
http_client,
auth_client,
path_manager: Arc::new(DoHPathManager::new(&globals)?),
path_manager,
odoh_configs,
})
}

Expand Down
3 changes: 2 additions & 1 deletion dap-lib/src/doh_client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod doh_client_healthcheck;
mod doh_client_main;
mod odoh_config_service;
mod odoh;
mod odoh_config_store;
mod path_manage;

pub use doh_client_main::DoHClient;
Expand Down
63 changes: 63 additions & 0 deletions dap-lib/src/doh_client/odoh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Based on https://github.com/DNSCrypt/doh-server/blob/master/src/libdoh/src/odoh.rs
use crate::{error::*, log::*};
use bytes::Bytes;
use odoh_rs::{
parse, ObliviousDoHConfigContents, ObliviousDoHConfigs, ObliviousDoHMessage, ObliviousDoHMessagePlaintext, OdohSecret,
};
use rand::{rngs::StdRng, SeedableRng};

#[derive(Debug, Clone)]
/// ODoH config
pub struct ODoHConfig {
authority: String,
inner: ObliviousDoHConfigContents,
}

impl ODoHConfig {
/// Create a new ODoHConfig
pub fn new(authority: &str, configs_vec: &[u8]) -> Result<Self> {
let odoh_configs: ObliviousDoHConfigs = parse(&mut (<&[u8]>::clone(&configs_vec)))?;
info!("[ODoH] Update ODoH configs: {authority}");
let client_config = match odoh_configs.into_iter().next() {
Some(t) => t,
None => return Err(DapError::ODoHNoClientConfig),
};
let inner: ObliviousDoHConfigContents = client_config.into();

Ok(ODoHConfig {
authority: authority.to_owned(),
inner,
})
}

/// Encrypt query
pub fn encrypt_query(&self, plaintext_query: &[u8]) -> Result<(ObliviousDoHMessagePlaintext, Bytes, OdohSecret)> {
debug!("[ODoH] Encrypt query");
let mut rng = StdRng::from_entropy();

// TODO: Padding bytes should be add? Padding be handled by a client issuing plaintext queries.
// add a random padding for testing purpose
// let padding_len = rng.gen_range(0..10);
// let query = ObliviousDoHMessagePlaintext::new(&plaintext_query, padding_len);
// debug!("[ODoH] Encrypting DNS message with {} bytes of padding", padding_len);
let query = ObliviousDoHMessagePlaintext::new(plaintext_query, 0);
let (query_enc, cli_secret) = odoh_rs::encrypt_query(&query, &self.inner, &mut rng)?;
let query_body = odoh_rs::compose(&query_enc)?.freeze();
Ok((query, query_body, cli_secret))
}

/// Decrypt response
pub fn decrypt_response(
&self,
plaintext_query: &ObliviousDoHMessagePlaintext,
encrypted_response: &Bytes,
client_secret: OdohSecret,
) -> Result<Bytes> {
debug!("[ODoH] Decrypt query");
let response_enc: ObliviousDoHMessage = parse(&mut (encrypted_response.clone()))?;
let response_dec = odoh_rs::decrypt_response(plaintext_query, &response_enc, client_secret)?;
debug!("[ODoH] Successfully decrypted");

Ok(response_dec.into_msg())
}
}
Empty file.
113 changes: 113 additions & 0 deletions dap-lib/src/doh_client/odoh_config_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use super::{odoh::ODoHConfig, path_manage::DoHTarget};
use crate::{
constants::{ODOH_CONFIG_PATH, ODOH_CONFIG_WATCH_DELAY},
error::*,
http_client::HttpClientInner,
log::*,
};
use rustc_hash::FxHashMap as HashMap;
use std::sync::Arc;
use tokio::{
sync::{Notify, RwLock},
time::{sleep, Duration},
};
use url::Url;

#[allow(clippy::complexity)]
/// ODoH config store
pub struct ODoHConfigStore {
inner: Arc<RwLock<HashMap<Arc<DoHTarget>, Arc<Option<ODoHConfig>>>>>,
http_client: Arc<RwLock<HttpClientInner>>,
}

impl ODoHConfigStore {
/// Create a new ODoHConfigStore
pub async fn new(http_client: Arc<RwLock<HttpClientInner>>, targets: &[Arc<DoHTarget>]) -> Result<Self> {
let inner = targets
.iter()
.map(|target| (target.clone(), Arc::new(None as Option<ODoHConfig>)))
.collect::<HashMap<_, _>>();
let res = Self {
inner: Arc::new(RwLock::new(inner)),
http_client,
};
res.update_odoh_config_from_well_known().await?;
Ok(res)
}

/// Fetch ODoHConfig from target
async fn update_odoh_config_from_well_known(&self) -> Result<()> {
// TODO: Add auth token when fetching config?
// fetch public key from odoh target (/.well-known)
let inner_lock = self.inner.read().await;
let inner = inner_lock.clone();
drop(inner_lock);

let futures = inner.keys().map(|target| async {
let mut destination = Url::parse(&format!("{}://{}", target.scheme(), target.authority())).unwrap();
destination.set_path(ODOH_CONFIG_PATH);
let lock = self.http_client.read().await;
debug!("Fetching ODoH config from {}", destination);
lock.get(destination).send().await
});
let joined = futures::future::join_all(futures);
let update_futures = joined.await.into_iter().zip(inner).map(|(res, current)| async move {
match res {
Ok(response) => {
if response.status() != reqwest::StatusCode::OK {
error!("Failed to fetch ODoH config!: {:?}", response.status());
return (current.0.clone(), Arc::new(None as Option<ODoHConfig>));
}
let Ok(body) = response.bytes().await else {
error!("Failed to parse response body in ODoH config response");
return (current.0.clone(), Arc::new(None as Option<ODoHConfig>));
};
let config = ODoHConfig::new(current.0.authority(), &body).ok();
(current.0.clone(), Arc::new(config))
}
Err(e) => {
error!("Failed to fetch ODoH config!: {:?}", e);
(current.0.clone(), Arc::new(None as Option<ODoHConfig>))
}
}
});
let update_joined = futures::future::join_all(update_futures)
.await
.into_iter()
.collect::<HashMap<_, _>>();
let mut inner_lock = self.inner.write().await;
*inner_lock = update_joined;
drop(inner_lock);
Ok(())
}

/// start odoh config watch service
pub(super) async fn start_service(&self, term_notify: Option<Arc<Notify>>) -> Result<()> {
info!("Start periodic odoh config watch service");
match term_notify {
Some(term) => {
tokio::select! {
_ = self.watch_service() => {
warn!("ODoH config watch service is down");
}
_ = term.notified() => {
info!("ODoH config watch service receives term signal");
}
}
}
None => {
self.watch_service().await?;
warn!("ODoH config watch service is down.");
}
}
Ok(())
}

/// watch service
async fn watch_service(&self) -> Result<()> {
loop {
self.update_odoh_config_from_well_known().await?;
sleep(Duration::from_secs(ODOH_CONFIG_WATCH_DELAY as u64)).await;
}
}
}
23 changes: 22 additions & 1 deletion dap-lib/src/doh_client/path_manage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::sync::{
};
use url::Url;

#[derive(Eq, PartialEq, Hash)]
/// scheme
enum Scheme {
Http,
Expand All @@ -31,15 +32,27 @@ impl TryFrom<&str> for Scheme {
}
}
}
#[derive(Eq, PartialEq, Hash)]
/// DoH target resolver
struct DoHTarget {
pub struct DoHTarget {
/// authority like "dns.google:443"
authority: String,
/// path like "/dns-query" that must start from "/"
path: String,
/// scheme
scheme: Scheme,
}
impl DoHTarget {
/// get authority
pub fn authority(&self) -> &str {
&self.authority
}
/// get scheme
pub fn scheme(&self) -> &str {
self.scheme.as_str()
}
}

/// ODoH and MODoH relay
struct DoHRelay {
/// authority like "dns.google:443"
Expand Down Expand Up @@ -159,6 +172,14 @@ pub struct DoHPathManager {
nexthop_randomization: bool,
}
impl DoHPathManager {
/// get target list
pub fn targets(&self) -> Vec<Arc<DoHTarget>> {
self
.paths
.iter()
.map(|per_target| per_target[0][0].target.clone())
.collect::<Vec<_>>()
}
/// get a healthy path according to the randomization policy
pub fn get_path(&self) -> Option<Arc<DoHPath>> {
let healthy_paths = self
Expand Down
6 changes: 6 additions & 0 deletions dap-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub enum DapError {
FailedToMakeDohQuery,
#[error("Failed to build DoH url")]
FailedToBuildDohUrl,
#[error("ODoH No Relay Url")]
ODoHNoRelayUrl,
#[error("ODoH No Client Config")]
ODoHNoClientConfig,
#[error("ODoH operation error")]
ODoHError(#[from] odoh_rs::Error),

#[error(transparent)]
Other(#[from] anyhow::Error),
Expand Down
2 changes: 1 addition & 1 deletion dap-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub async fn entrypoint(
}

// build doh_client
let doh_client = Arc::new(DoHClient::new(globals.clone(), http_client.inner(), authenticator)?);
let doh_client = Arc::new(DoHClient::new(globals.clone(), http_client.inner(), authenticator).await?);

// spawn endpoint ip update service with bootstrap dns resolver and doh_client
let doh_client_clone = doh_client.clone();
Expand Down
2 changes: 1 addition & 1 deletion dap-lib/src/proxy/proxy_udp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl Proxy {
}
None => {
service.await;
warn!("Auth service got down");
warn!("Udp responder service got down");
}
}
}
Expand Down

0 comments on commit 8a87d35

Please sign in to comment.