Skip to content

Commit

Permalink
Merge pull request #5 from junkurihara/feat/ip-domain-filtering
Browse files Browse the repository at this point in the history
Feat ip domain filtering
  • Loading branch information
junkurihara committed Dec 13, 2023
2 parents f0b5837 + 7de76eb commit 4174f69
Show file tree
Hide file tree
Showing 16 changed files with 643 additions and 28 deletions.
5 changes: 4 additions & 1 deletion modoh-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ anyhow = "1.0.75"
mimalloc = { version = "*", default-features = false }
serde = { version = "1.0.193", default-features = false, features = ["derive"] }
derive_builder = "0.12.0"
tokio = { version = "1.34.0", default-features = false, features = [
tokio = { version = "1.35.0", default-features = false, features = [
"net",
"rt-multi-thread",
"time",
Expand All @@ -56,3 +56,6 @@ hot_reload = "0.1.4"
# logging
tracing = { version = "0.1.40" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

# ip address
ipnet = { version = "2.9.0" }
58 changes: 54 additions & 4 deletions modoh-bin/src/config/target_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ use super::toml::ConfigToml;
use crate::{error::*, log::*};
use async_trait::async_trait;
use hot_reload::{Reload, ReloaderError};
use ipnet::IpNet;
use modoh_server_lib::{AccessConfig, ServiceConfig, ValidationConfig, ValidationConfigInner};
use std::net::{IpAddr, SocketAddr};
use std::{
fs::read_to_string,
net::{IpAddr, SocketAddr},
};

#[derive(PartialEq, Eq, Clone, Debug)]
/// Wrapper of config toml and manipulation plugin settings
Expand Down Expand Up @@ -154,15 +158,30 @@ impl TryInto<ServiceConfig> for &TargetConfig {

if let Some(access) = self.config_toml.access.as_ref() {
let mut inner_ip = vec![];
for ip in access.allowed_source_ip_addresses.iter() {
let ip = ip.parse::<IpAddr>()?;
for ip in access.allowed_source_ips.as_ref().unwrap_or(&vec![]).iter() {
let ip = parse_ipnet(ip)?;
info!("Set allowed source ip address: {}", ip);
inner_ip.push(ip);
}

let mut inner_cdn_ip = vec![];
for ip in access.trusted_cdn_ips.as_ref().unwrap_or(&vec![]).iter() {
let ip = ip.parse::<IpNet>()?;
info!("Set trusted cdn ip address: {}", ip);
inner_cdn_ip.push(ip);
}
if let Some(cdn_ip_list_path) = access.trusted_cdn_ips_file.as_ref() {
let ip_list = read_ipnet_list_from_file(cdn_ip_list_path)?;
info!("Set trusted cdn ip address from file: {:#?}", ip_list);
inner_cdn_ip.extend(ip_list);
}

let trust_previous_hop = access.trust_previous_hop.unwrap_or(true);
info!("Set trust previous hop: {}", trust_previous_hop);

let mut inner_domain = vec![];
if service_conf.relay.is_some() {
for domain in access.allowed_destination_domains.iter() {
for domain in access.allowed_destination_domains.as_ref().unwrap_or(&vec![]).iter() {
let domain = url::Url::parse(&format!("https://{domain}"))?
.authority()
.to_ascii_lowercase();
Expand All @@ -174,9 +193,40 @@ impl TryInto<ServiceConfig> for &TargetConfig {
service_conf.access = Some(AccessConfig {
allowed_source_ip_addresses: inner_ip,
allowed_destination_domains: inner_domain,
trusted_cdn_ip_addresses: inner_cdn_ip,
trust_previous_hop,
});
};

Ok(service_conf)
}
}

/// parse ipnet from string
fn parse_ipnet(ip: &str) -> anyhow::Result<IpNet> {
if ip.contains('/') {
let ip = ip.parse::<IpNet>()?;
return Ok(ip);
}
let ip = ip.parse::<IpAddr>()?;
Ok(IpNet::from(ip))
}

/// read ipnet list from file
fn read_ipnet_list_from_file(path: &str) -> anyhow::Result<Vec<IpNet>> {
let list_lines = read_to_string(path)?;

let ip_list = list_lines
.lines()
.filter_map(|line| {
let line_without_comment = line.trim().split('#').next().unwrap_or("").trim();
if line_without_comment.is_empty() {
None
} else {
parse_ipnet(line_without_comment).ok()
}
})
.collect::<Vec<_>>();

Ok(ip_list)
}
12 changes: 10 additions & 2 deletions modoh-bin/src/config/toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,17 @@ pub struct Token {
/// Allowed source ip addresses and destination domains
pub struct Access {
/// Allowed source ip addresses
pub allowed_source_ip_addresses: Vec<String>,
pub allowed_source_ips: Option<Vec<String>>,

/// Trusted cdn ip addresses
pub trusted_cdn_ips: Option<Vec<String>>,
/// Trusted cdn ip addresses file
pub trusted_cdn_ips_file: Option<String>,
/// Always trust previous proxy address retrieved from remote_addr
pub trust_previous_hop: Option<bool>,

/// Allowed destination domains
pub allowed_destination_domains: Vec<String>,
pub allowed_destination_domains: Option<Vec<String>>,
}

impl ConfigToml {
Expand Down
7 changes: 6 additions & 1 deletion modoh-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ futures = { version = "0.3.29", default-features = false, features = [
"std",
"async-await",
] }
tokio = { version = "1.34.0", features = [
tokio = { version = "1.35.0", features = [
"net",
"rt-multi-thread",
"time",
Expand Down Expand Up @@ -83,3 +83,8 @@ byteorder = "1.5.0"
serde = { version = "1.0.193", default-features = false }
auth-validator = { git = "https://github.com/junkurihara/rust-token-server", package = "rust-token-server-validator", branch = "develop" }
serde_json = { version = "1.0.108" }

# access control
ipnet = { version = "2.9.0" }
cedarwood = { version = "0.4.6" }
regex = { version = "1.10.2" }
13 changes: 13 additions & 0 deletions modoh-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ pub enum HttpError {
#[error("Invalid token")]
InvalidToken,

#[error("Invalid forwarded header: {0}")]
InvalidForwardedHeader(String),
#[error("Invalid X-Forwarded-For header: {0}")]
InvalidXForwardedForHeader(String),
#[error("Forbidden source ip address: {0}")]
ForbiddenSourceAddress(String),
#[error("Forbidden destination domain: {0}")]
ForbiddenDomain(String),

#[error(transparent)]
Other(#[from] anyhow::Error),
}
Expand Down Expand Up @@ -142,6 +151,10 @@ impl From<HttpError> for StatusCode {
HttpError::NoAuthorizationHeader => StatusCode::FORBIDDEN,
HttpError::InvalidAuthorizationHeader => StatusCode::FORBIDDEN,
HttpError::InvalidToken => StatusCode::UNAUTHORIZED,
HttpError::ForbiddenSourceAddress(_) => StatusCode::FORBIDDEN,
HttpError::ForbiddenDomain(_) => StatusCode::FORBIDDEN,

HttpError::InvalidForwardedHeader(_) => StatusCode::BAD_REQUEST,

_ => StatusCode::INTERNAL_SERVER_ERROR,
}
Expand Down
9 changes: 7 additions & 2 deletions modoh-lib/src/globals.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{constants::*, count::RequestCount};
use auth_validator::ValidationConfig;
use ipnet::IpNet;
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
sync::Arc,
time::Duration,
};
Expand Down Expand Up @@ -87,9 +88,13 @@ pub struct TargetConfig {
/// Allowed source ip addresses and destination domains
pub struct AccessConfig {
/// Allowed source ip addresses
pub allowed_source_ip_addresses: Vec<IpAddr>,
pub allowed_source_ip_addresses: Vec<IpNet>,
/// Allowed destination domains
pub allowed_destination_domains: Vec<String>,
/// Trusted CDN ip addresses
pub trusted_cdn_ip_addresses: Vec<IpNet>,
/// Whether to trust previous hop reverse proxy
pub trust_previous_hop: bool,
}

impl Default for ServiceConfig {
Expand Down
4 changes: 2 additions & 2 deletions modoh-lib/src/hyper_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Use this just for testing. Please enable native-tls or rustls feature to enable
}
}

#[cfg(feature = "native-tls")]
#[cfg(all(feature = "native-tls", not(feature = "rustls")))]
impl<B> HttpClient<hyper_tls::HttpsConnector<HttpConnector>, B>
where
B: Body + Send + Unpin + 'static,
Expand Down Expand Up @@ -102,7 +102,7 @@ where
<B as Body>::Error: Into<Box<(dyn std::error::Error + Send + Sync + 'static)>>,
{
/// Build forwarder
pub async fn try_new(runtime_handle: tokio::runtime::Handle) -> Result<Self> {
pub fn try_new(runtime_handle: tokio::runtime::Handle) -> Result<Self> {
todo!("Not implemented yet. Please use native-tls-backend feature for now.");

// build hyper client with rustls and webpki, only https is allowed
Expand Down
3 changes: 2 additions & 1 deletion modoh-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod hyper_executor;
mod log;
mod message_util;
mod relay;
mod request_filter;
mod router;
mod target;
mod validator;
Expand All @@ -28,7 +29,7 @@ pub async fn entrypoint(
term_notify: Option<Arc<tokio::sync::Notify>>,
) -> Result<()> {
#[cfg(all(feature = "rustls", feature = "native-tls"))]
warn!("Both \"native-tls\" and feature \"rustls\" features are enabled. \"native-tls\" will be used.");
warn!("Both \"native-tls\" and feature \"rustls\" features are enabled. \"rustls\" will be used.");

// build globals
let globals = Arc::new(Globals {
Expand Down
1 change: 1 addition & 0 deletions modoh-lib/src/relay/relay_handle_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ mod tests {
relay_host: "example.com".to_string(),
relay_path: "/proxy".to_string(),
max_subseq_nodes: 3,
request_filter: None,
};

let url = Url::parse("https://example.com/proxy?targethost=example1.com&targetpath=/dns-query").unwrap();
Expand Down
27 changes: 25 additions & 2 deletions modoh-lib/src/relay/relay_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{
hyper_client::HttpClient,
log::*,
message_util::{check_content_type, inspect_host, inspect_request_body, RequestType},
request_filter::RequestFilter,
};
use http::{
header::{self, HeaderMap, HeaderValue},
Expand Down Expand Up @@ -35,6 +36,8 @@ where
pub(crate) relay_path: String,
/// max number of subsequent nodes
pub(super) max_subseq_nodes: usize,
/// request filter for destination domain name
pub(super) request_filter: Option<Arc<RequestFilter>>,
}

impl<C, B> InnerRelay<C, B>
Expand Down Expand Up @@ -86,8 +89,23 @@ where
};
debug!("(M)ODoH next hop url: {}", nexthop_url.as_str());

// TODO: next hop domain name check here?
// next hop domain name check here
// for authorized domains, maintain blacklist (error metrics) at each relay for given responses
let nexthop_domain = nexthop_url.host_str().ok_or(HttpError::InvalidUrl)?;
let filter_result = self.request_filter.as_ref().and_then(|filter| {
filter
.outbound_filter
.as_ref()
.map(|outbound| outbound.in_domain_list(nexthop_domain))
});
if let Some(res) = filter_result {
if !res {
debug!("Nexthop domain is filtered: {}", nexthop_domain);
return Err(HttpError::ForbiddenDomain(nexthop_domain.to_string()));
}

debug!("Passed destination domain access control");
}

// split request into parts and body to manipulate them later
let (mut parts, body) = req.into_parts();
Expand Down Expand Up @@ -158,7 +176,11 @@ where
}

/// Build inner relay
pub fn try_new(globals: &Arc<Globals>, http_client: &Arc<HttpClient<C, B>>) -> Result<Arc<Self>> {
pub fn try_new(
globals: &Arc<Globals>,
http_client: &Arc<HttpClient<C, B>>,
request_filter: Option<Arc<RequestFilter>>,
) -> Result<Arc<Self>> {
let relay_config = globals
.service_config
.relay
Expand All @@ -185,6 +207,7 @@ where
relay_host,
relay_path,
max_subseq_nodes,
request_filter: request_filter.clone(),
}))
}
}
Loading

0 comments on commit 4174f69

Please sign in to comment.