Skip to content

Commit

Permalink
Merge pull request #72 from junkurihara/feat/not-forwarded-arpa
Browse files Browse the repository at this point in the history
Feat/not forwarded arpa
  • Loading branch information
junkurihara authored Dec 21, 2024
2 parents 1a3c60c + df1f41f commit 05df930
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 51 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
You should also include the user name that made the change.
-->

## 0.4.0 (Unreleased)
## 0.4.2 (Unreleased)

## 0.4.1

- Feat: support handling not-forwarded domains and local domains by default. For example, `resolver.arpa` is not forwarded to the upstream resolver, and `localhost` is always resolved to `127.0.0.1` or `::1`.
- Refactor: Various minor improvements
- Deps.

## 0.4.0

- Feat: Support anonymous token based on blind RSA signatures.
- Feat: DNS query logging (`qrlog` feature)
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ members = ["proxy-bin", "proxy-lib"]
resolver = "2"

[workspace.package]
version = "0.4.1"
version = "0.4.2"
authors = ["Jun Kurihara"]
homepage = "https://github.com/junkurihara/doh-auth-proxy"
repository = "https://github.com/junkurihara/doh-auth-proxy"
Expand Down
4 changes: 4 additions & 0 deletions proxy-lib/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ pub const HEALTHCHECK_TARGET_ADDR: &str = "8.8.8.8";
pub const BLOCK_MESSAGE_HINFO_CPU: &str = "BLOCKED";
/// Block message for query manipulation (HINFO OS field)
pub const BLOCK_MESSAGE_HINFO_OS: &str = "POWERED-BY-DOH-AUTH-PROXY";
/// Not-forwarded message for query manipulation (HINFO CPU field)
pub const NOT_FORWARDED_MESSAGE_HINFO_CPU: &str = "NOT-FORWARDED-BY-DEFAULT";
/// Not-forwarded message for query manipulation (HINFO OS field)
pub const NOT_FORWARDED_MESSAGE_HINFO_OS: &str = "POWERED-BY-DOH-AUTH-PROXY";

// Logging
/// Query log channel size
Expand Down
8 changes: 8 additions & 0 deletions proxy-lib/src/doh_client/dns_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ pub fn build_response_nx(msg: &Message) -> Message {
res
}

/// Build a DNS response message with REFUSED
pub fn build_response_refused(msg: &Message) -> Message {
let mut res = msg.clone();
res.set_message_type(hickory_proto::op::MessageType::Response);
res.set_response_code(hickory_proto::op::ResponseCode::Refused);
res
}

/// Build a DNS response message for given QueryKey and IP address
pub fn build_response_given_ipaddr(msg: &Message, q_key: &QueryKey, ipaddr: &IpAddr, min_ttl: u32) -> anyhow::Result<Message> {
let mut res = msg.clone();
Expand Down
45 changes: 27 additions & 18 deletions proxy-lib/src/doh_client/doh_client_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub struct DoHClient {
/// health check interval
pub(super) healthcheck_period_sec: tokio::time::Duration,
/// Query manipulation pulugins
query_manipulators: Option<QueryManipulators>,
query_manipulators: QueryManipulators,
/// Query logging sender
query_log_tx: crossbeam_channel::Sender<QueryLoggingBase>,
}
Expand Down Expand Up @@ -119,10 +119,10 @@ impl DoHClient {
let healthcheck_period_sec = globals.proxy_config.healthcheck_period_sec;

// query manipulators
let query_manipulators: Option<QueryManipulators> = if let Some(q) = &globals.proxy_config.query_manipulation_config {
q.as_ref().try_into().ok()
let query_manipulators: QueryManipulators = if let Some(q) = &globals.proxy_config.query_manipulation_config {
q.as_ref().try_into().unwrap_or_default()
} else {
None
QueryManipulators::default()
};

Ok(Self {
Expand Down Expand Up @@ -186,20 +186,29 @@ impl DoHClient {
})?;

// Process query plugins from the beginning of vec, e.g., domain filtering, cloaking, etc.
if let Some(manipulators) = &self.query_manipulators {
let execution_result = manipulators.apply(&query_msg, &req.0[0]).await?;
match execution_result {
QueryManipulationResult::PassThrough => (),
QueryManipulationResult::SyntheticResponseBlocked(response_msg) => {
let res = dns_message::encode(&response_msg)?;
self.log_dns_message(&res, proto, src, DoHResponseType::Blocked, None, start);
return Ok(res);
}
QueryManipulationResult::SyntheticResponseOverridden(response_msg) => {
let res = dns_message::encode(&response_msg)?;
self.log_dns_message(&res, proto, src, DoHResponseType::Overridden, None, start);
return Ok(res);
}

let execution_result = self.query_manipulators.apply(&query_msg, &req.0[0]).await?;
match execution_result {
QueryManipulationResult::PassThrough => (),
QueryManipulationResult::SyntheticResponseBlocked(response_msg) => {
let res = dns_message::encode(&response_msg)?;
self.log_dns_message(&res, proto, src, DoHResponseType::Blocked, None, start);
return Ok(res);
}
QueryManipulationResult::SyntheticResponseOverridden(response_msg) => {
let res = dns_message::encode(&response_msg)?;
self.log_dns_message(&res, proto, src, DoHResponseType::Overridden, None, start);
return Ok(res);
}
QueryManipulationResult::SyntheticResponseNotForwarded(response_msg) => {
let res = dns_message::encode(&response_msg)?;
self.log_dns_message(&res, proto, src, DoHResponseType::NotForwarded, None, start);
return Ok(res);
}
QueryManipulationResult::SyntheticResponseDefaultHost(response_msg) => {
let res = dns_message::encode(&response_msg)?;
self.log_dns_message(&res, proto, src, DoHResponseType::DefaultHost, None, start);
return Ok(res);
}
}

Expand Down
159 changes: 159 additions & 0 deletions proxy-lib/src/doh_client/manipulation/default_rule.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use super::{
super::{
dns_message::{build_response_given_ipaddr, build_response_refused, QueryKey},
error::DohClientError,
},
inspect_query_name, QueryManipulation, QueryManipulationResult,
};
use crate::{
constants::{NOT_FORWARDED_MESSAGE_HINFO_CPU, NOT_FORWARDED_MESSAGE_HINFO_OS},
log::*,
};
use async_trait::async_trait;
use hickory_proto::{op::Message, rr};
use match_domain::DomainMatchingRule;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

/* -------------------------------------------------------- */
/// Default not-forwarded domains
const DEFAULT_NOT_FORWARDED_DOMAINS: &[&str] = &[
// https://www.rfc-editor.org/rfc/rfc9462.html#name-caching-forwarders
"resolver.arpa",
];
/// Default localhost
const DEFAULT_LOCAL_DOMAINS: &[&str] = &["localhost", "localhost.localdomain"];
/// Default broadcast
const DEFAULT_BROADCAST_DOMAINS: &[&str] = &["broadcasthost"];

#[inline]
fn build_local_v4_response(query_message: &Message, query_key: &QueryKey) -> anyhow::Result<Message> {
let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
build_response_given_ipaddr(query_message, query_key, &addr, 0)
}
#[inline]
fn build_local_v6_response(query_message: &Message, query_key: &QueryKey) -> anyhow::Result<Message> {
let addr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
build_response_given_ipaddr(query_message, query_key, &addr, 0)
}
#[inline]
// only v4
fn build_broadcast_response(query_message: &Message, query_key: &QueryKey) -> anyhow::Result<Message> {
let addr = IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255));
build_response_given_ipaddr(query_message, query_key, &addr, 0)
}
/* -------------------------------------------------------- */

#[async_trait]
impl QueryManipulation for DefaultRule {
type Error = DohClientError;

/// Apply query plugin
async fn apply(&self, query_message: &Message, query_key: &QueryKey) -> Result<QueryManipulationResult, DohClientError> {
let q_name = inspect_query_name(&query_key.query_name)?;

// Check if the query is for not-forwarded domains
if self.is_not_forwarded(q_name.as_str())? {
debug!(
"[Not-Forwarded] {} {:?} {:?}",
query_key.query_name, query_key.query_type, query_key.query_class
);
let response_msg = build_response_not_forwarded(query_message);
return Ok(QueryManipulationResult::SyntheticResponseNotForwarded(response_msg));
}

// Check if the query is for localhost
if self.is_localhost(q_name.as_str()) {
debug!(
"[LocalHost] {} {:?} {:?}",
query_key.query_name, query_key.query_type, query_key.query_class
);

let response_msg = match query_key.query_type {
rr::RecordType::A => build_local_v4_response(query_message, query_key)?,
rr::RecordType::AAAA => build_local_v6_response(query_message, query_key)?,
_ => build_response_refused(query_message),
};
return Ok(QueryManipulationResult::SyntheticResponseDefaultHost(response_msg));
}

// Check if the query is for broadcast
if self.is_broadcast(q_name.as_str()) {
debug!(
"[Broadcast] {} {:?} {:?}",
query_key.query_name, query_key.query_type, query_key.query_class
);

let response_msg = match query_key.query_type {
rr::RecordType::A => build_broadcast_response(query_message, query_key)?,
_ => build_response_refused(query_message),
};
return Ok(QueryManipulationResult::SyntheticResponseDefaultHost(response_msg));
}

return Ok(QueryManipulationResult::PassThrough);
}
}

/// Build a synthetic response message for default not-forwarded domains
fn build_response_not_forwarded(query_message: &Message) -> Message {
let mut msg = build_response_refused(query_message);
let hinfo = rr::rdata::HINFO::new(
NOT_FORWARDED_MESSAGE_HINFO_CPU.to_string(),
NOT_FORWARDED_MESSAGE_HINFO_OS.to_string(),
);
msg.add_answer(rr::Record::from_rdata(
query_message.queries()[0].name().clone(),
0,
rr::RData::HINFO(hinfo),
));
msg
}

#[derive(Debug, Clone)]
/// NotForwardedRule is a query manipulation rule that refuses queries based on domain matching
/// This is a default rule, handling the regulations of IETF RFC
pub struct DefaultRule {
/// inner domain matching rule
not_forwarded: DomainMatchingRule,
}

impl DefaultRule {
/// Create a new NotForwardedRule
pub fn new() -> Self {
let not_forwarded = DomainMatchingRule::try_from(DEFAULT_NOT_FORWARDED_DOMAINS).unwrap();
DefaultRule { not_forwarded }
}

/// Check if the query key is in blocklist
fn is_not_forwarded(&self, q_name: &str) -> anyhow::Result<bool> {
Ok(self.not_forwarded.is_matched(q_name))
}

/// Check if the query key is for localhost
fn is_localhost(&self, q_name: &str) -> bool {
DEFAULT_LOCAL_DOMAINS.iter().any(|&d| d.eq(q_name))
}
/// Check if the query key is for broadcast
fn is_broadcast(&self, q_name: &str) -> bool {
DEFAULT_BROADCAST_DOMAINS.iter().any(|&d| d.eq(q_name))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_works() {
let default_rule = DefaultRule::new();

assert!(default_rule.is_not_forwarded("resolver.arpa").unwrap());
assert!(default_rule.is_not_forwarded("_dns.resolver.arpa").unwrap());
assert!(default_rule.is_localhost("localhost"));
assert!(default_rule.is_localhost("localhost.localdomain"));
assert!(!default_rule.is_localhost("localhost.localdomain.com"));
assert!(!default_rule.is_localhost("x.localhost.localdomain"));
assert!(default_rule.is_broadcast("broadcasthost"));
assert!(!default_rule.is_broadcast("broadcasthost.com"));
}
}
18 changes: 3 additions & 15 deletions proxy-lib/src/doh_client/manipulation/domain_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ use super::{
dns_message::{build_response_nx, QueryKey},
error::DohClientError,
},
QueryManipulation, QueryManipulationResult,
inspect_query_name, QueryManipulation, QueryManipulationResult,
};
use crate::{
constants::{BLOCK_MESSAGE_HINFO_CPU, BLOCK_MESSAGE_HINFO_OS},
log::*,
QueryManipulationConfig,
};
use anyhow::bail;
use async_trait::async_trait;
use hickory_proto::{op::Message, rr};
use match_domain::DomainMatchingRule;
Expand Down Expand Up @@ -67,19 +66,8 @@ impl TryFrom<&QueryManipulationConfig> for Option<DomainBlockRule> {
impl DomainBlockRule {
/// Check if the query key is in blocklist
pub fn in_blocklist(&self, q_key: &QueryKey) -> anyhow::Result<bool> {
// remove final dot
let mut nn = q_key.clone().query_name.to_ascii_lowercase();
match nn.pop() {
Some(dot) => {
if dot != '.' {
bail!("Invalid query name as fqdn (missing final dot): {}", nn);
}
}
None => {
bail!("Missing query name");
}
}

// remove final dot and convert to lowercase
let nn = inspect_query_name(q_key.query_name.as_str())?;
Ok(self.inner.is_matched(&nn))
}
}
Expand Down
18 changes: 5 additions & 13 deletions proxy-lib/src/doh_client/manipulation/domain_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::{
dns_message::{build_response_given_ipaddr, QueryKey},
error::DohClientError,
},
inspect_query_name,
regexp_vals::*,
QueryManipulation, QueryManipulationResult,
};
Expand Down Expand Up @@ -95,19 +96,10 @@ impl TryFrom<&QueryManipulationConfig> for Option<DomainOverrideRule> {
impl DomainOverrideRule {
pub fn find_mapping(&self, q_key: &QueryKey) -> Option<&MapsTo> {
let q_type = q_key.query_type;
// remove final dot
let mut nn = q_key.clone().query_name.to_ascii_lowercase();
match nn.pop() {
Some(dot) => {
if dot != '.' {
return None;
}
}
None => {
warn!("Null request!");
return None;
}
}

// remove final dot and convert to lowercase
let nn = inspect_query_name(q_key.query_name.as_str()).ok()?;

// find matches
if let Some(targets) = self.inner.get(&nn) {
targets.iter().find(|x| match x {
Expand Down
Loading

0 comments on commit 05df930

Please sign in to comment.