Merge pull request #101 from razvandimescu/fix/forward-tls-upstream
fix: accept tls:// and https:// in [[forwarding]] upstreams
This commit was merged in pull request #101.
This commit is contained in:
@@ -58,6 +58,14 @@ api_port = 5380
|
|||||||
# [[forwarding]]
|
# [[forwarding]]
|
||||||
# suffix = ["home.local", "home.arpa"] # multiple suffixes → same upstream
|
# suffix = ["home.local", "home.arpa"] # multiple suffixes → same upstream
|
||||||
# upstream = "10.0.0.1" # port 53 default
|
# upstream = "10.0.0.1" # port 53 default
|
||||||
|
#
|
||||||
|
# [[forwarding]] # DoT upstream: tls://IP[:port]#hostname
|
||||||
|
# suffix = ["google.com", "goog"] # hostname is the TLS SNI / cert name
|
||||||
|
# upstream = "tls://9.9.9.9#dns.quad9.net" # port 853 default
|
||||||
|
#
|
||||||
|
# [[forwarding]] # DoH upstream: full https:// URL
|
||||||
|
# suffix = "example.corp"
|
||||||
|
# upstream = "https://dns.quad9.net/dns-query"
|
||||||
|
|
||||||
# [blocking]
|
# [blocking]
|
||||||
# enabled = true # set to false to disable ad blocking
|
# enabled = true # set to false to disable ad blocking
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ pub struct ForwardingRuleConfig {
|
|||||||
|
|
||||||
impl ForwardingRuleConfig {
|
impl ForwardingRuleConfig {
|
||||||
fn to_runtime_rules(&self) -> Result<Vec<crate::system_dns::ForwardingRule>> {
|
fn to_runtime_rules(&self) -> Result<Vec<crate::system_dns::ForwardingRule>> {
|
||||||
let addr = crate::forward::parse_upstream_addr(&self.upstream, 53)
|
let upstream = crate::forward::parse_upstream(&self.upstream, 53)
|
||||||
.map_err(|e| format!("forwarding rule for upstream '{}': {}", self.upstream, e))?;
|
.map_err(|e| format!("forwarding rule for upstream '{}': {}", self.upstream, e))?;
|
||||||
Ok(self
|
Ok(self
|
||||||
.suffix
|
.suffix
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| crate::system_dns::ForwardingRule::new(s.clone(), addr))
|
.map(|s| crate::system_dns::ForwardingRule::new(s.clone(), upstream.clone()))
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -710,6 +710,10 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let runtime = rule.to_runtime_rules().unwrap();
|
let runtime = rule.to_runtime_rules().unwrap();
|
||||||
assert_eq!(runtime.len(), 1);
|
assert_eq!(runtime.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
runtime[0].upstream,
|
||||||
|
crate::forward::Upstream::Udp(_)
|
||||||
|
));
|
||||||
assert_eq!(runtime[0].upstream.to_string(), "100.90.1.63:5361");
|
assert_eq!(runtime[0].upstream.to_string(), "100.90.1.63:5361");
|
||||||
assert_eq!(runtime[0].suffix, "home.local");
|
assert_eq!(runtime[0].suffix, "home.local");
|
||||||
}
|
}
|
||||||
@@ -733,6 +737,38 @@ mod tests {
|
|||||||
assert!(rule.to_runtime_rules().is_err());
|
assert!(rule.to_runtime_rules().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forwarding_upstream_accepts_dot_scheme() {
|
||||||
|
let rule = ForwardingRuleConfig {
|
||||||
|
suffix: vec!["google.com".to_string()],
|
||||||
|
upstream: "tls://9.9.9.9#dns.quad9.net".to_string(),
|
||||||
|
};
|
||||||
|
let runtime = rule
|
||||||
|
.to_runtime_rules()
|
||||||
|
.expect("tls:// upstream should parse");
|
||||||
|
assert_eq!(runtime.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
runtime[0].upstream.to_string(),
|
||||||
|
"tls://9.9.9.9:853#dns.quad9.net"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forwarding_upstream_accepts_doh_scheme() {
|
||||||
|
let rule = ForwardingRuleConfig {
|
||||||
|
suffix: vec!["goog".to_string()],
|
||||||
|
upstream: "https://dns.quad9.net/dns-query".to_string(),
|
||||||
|
};
|
||||||
|
let runtime = rule
|
||||||
|
.to_runtime_rules()
|
||||||
|
.expect("https:// upstream should parse");
|
||||||
|
assert_eq!(runtime.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
runtime[0].upstream.to_string(),
|
||||||
|
"https://dns.quad9.net/dns-query"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn forwarding_config_rules_take_precedence_over_discovered() {
|
fn forwarding_config_rules_take_precedence_over_discovered() {
|
||||||
let config_rules = vec![ForwardingRuleConfig {
|
let config_rules = vec![ForwardingRuleConfig {
|
||||||
@@ -741,7 +777,7 @@ mod tests {
|
|||||||
}];
|
}];
|
||||||
let discovered = vec![crate::system_dns::ForwardingRule::new(
|
let discovered = vec![crate::system_dns::ForwardingRule::new(
|
||||||
"home.local".to_string(),
|
"home.local".to_string(),
|
||||||
"192.168.1.1:53".parse().unwrap(),
|
crate::forward::Upstream::Udp("192.168.1.1:53".parse().unwrap()),
|
||||||
)];
|
)];
|
||||||
let merged = merge_forwarding_rules(&config_rules, discovered).unwrap();
|
let merged = merge_forwarding_rules(&config_rules, discovered).unwrap();
|
||||||
let picked = crate::system_dns::match_forwarding_rule("host.home.local", &merged)
|
let picked = crate::system_dns::match_forwarding_rule("host.home.local", &merged)
|
||||||
@@ -757,7 +793,7 @@ mod tests {
|
|||||||
}];
|
}];
|
||||||
let discovered = vec![crate::system_dns::ForwardingRule::new(
|
let discovered = vec![crate::system_dns::ForwardingRule::new(
|
||||||
"corp.example".to_string(),
|
"corp.example".to_string(),
|
||||||
"192.168.1.1:53".parse().unwrap(),
|
crate::forward::Upstream::Udp("192.168.1.1:53".parse().unwrap()),
|
||||||
)];
|
)];
|
||||||
let merged = merge_forwarding_rules(&config_rules, discovered).unwrap();
|
let merged = merge_forwarding_rules(&config_rules, discovered).unwrap();
|
||||||
assert_eq!(merged.len(), 2);
|
assert_eq!(merged.len(), 2);
|
||||||
|
|||||||
12
src/ctx.rs
12
src/ctx.rs
@@ -190,13 +190,12 @@ pub async fn resolve_query(
|
|||||||
resp.header.authed_data = true;
|
resp.header.authed_data = true;
|
||||||
}
|
}
|
||||||
(resp, QueryPath::Cached, cached_dnssec)
|
(resp, QueryPath::Cached, cached_dnssec)
|
||||||
} else if let Some(fwd_addr) =
|
} else if let Some(upstream) =
|
||||||
crate::system_dns::match_forwarding_rule(&qname, &ctx.forwarding_rules)
|
crate::system_dns::match_forwarding_rule(&qname, &ctx.forwarding_rules)
|
||||||
{
|
{
|
||||||
// Conditional forwarding takes priority over recursive mode
|
// Conditional forwarding takes priority over recursive mode
|
||||||
// (e.g. Tailscale .ts.net, VPC private zones)
|
// (e.g. Tailscale .ts.net, VPC private zones)
|
||||||
let upstream = Upstream::Udp(fwd_addr);
|
match forward_and_cache(raw_wire, upstream, ctx, &qname, qtype).await {
|
||||||
match forward_and_cache(raw_wire, &upstream, ctx, &qname, qtype).await {
|
|
||||||
Ok(resp) => (resp, QueryPath::Forwarded, DnssecStatus::Indeterminate),
|
Ok(resp) => (resp, QueryPath::Forwarded, DnssecStatus::Indeterminate),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -1083,7 +1082,7 @@ mod tests {
|
|||||||
let mut ctx = crate::testutil::test_ctx().await;
|
let mut ctx = crate::testutil::test_ctx().await;
|
||||||
ctx.forwarding_rules = vec![ForwardingRule::new(
|
ctx.forwarding_rules = vec![ForwardingRule::new(
|
||||||
"168.192.in-addr.arpa".to_string(),
|
"168.192.in-addr.arpa".to_string(),
|
||||||
upstream_addr,
|
Upstream::Udp(upstream_addr),
|
||||||
)];
|
)];
|
||||||
let ctx = Arc::new(ctx);
|
let ctx = Arc::new(ctx);
|
||||||
|
|
||||||
@@ -1236,7 +1235,10 @@ mod tests {
|
|||||||
let upstream_addr = crate::testutil::mock_upstream(upstream_resp).await;
|
let upstream_addr = crate::testutil::mock_upstream(upstream_resp).await;
|
||||||
|
|
||||||
let mut ctx = crate::testutil::test_ctx().await;
|
let mut ctx = crate::testutil::test_ctx().await;
|
||||||
ctx.forwarding_rules = vec![ForwardingRule::new("corp".to_string(), upstream_addr)];
|
ctx.forwarding_rules = vec![ForwardingRule::new(
|
||||||
|
"corp".to_string(),
|
||||||
|
Upstream::Udp(upstream_addr),
|
||||||
|
)];
|
||||||
let ctx = Arc::new(ctx);
|
let ctx = Arc::new(ctx);
|
||||||
|
|
||||||
let (resp, path) = resolve_in_test(&ctx, "internal.corp", QueryType::A).await;
|
let (resp, path) = resolve_in_test(&ctx, "internal.corp", QueryType::A).await;
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ impl PartialEq for Upstream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Upstream {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt::Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for Upstream {
|
impl fmt::Display for Upstream {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -49,7 +55,10 @@ impl fmt::Display for Upstream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_upstream_addr(s: &str, default_port: u16) -> std::result::Result<SocketAddr, String> {
|
pub(crate) fn parse_upstream_addr(
|
||||||
|
s: &str,
|
||||||
|
default_port: u16,
|
||||||
|
) -> std::result::Result<SocketAddr, String> {
|
||||||
// Try full socket addr first: "1.2.3.4:5353" or "[::1]:5353"
|
// Try full socket addr first: "1.2.3.4:5353" or "[::1]:5353"
|
||||||
if let Ok(addr) = s.parse::<SocketAddr>() {
|
if let Ok(addr) = s.parse::<SocketAddr>() {
|
||||||
return Ok(addr);
|
return Ok(addr);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use std::net::SocketAddr;
|
|||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
use crate::forward::Upstream;
|
||||||
|
|
||||||
fn print_recursive_hint() {
|
fn print_recursive_hint() {
|
||||||
let is_recursive = crate::config::load_config("numa.toml")
|
let is_recursive = crate::config::load_config("numa.toml")
|
||||||
.map(|c| c.config.upstream.mode == crate::config::UpstreamMode::Recursive)
|
.map(|c| c.config.upstream.mode == crate::config::UpstreamMode::Recursive)
|
||||||
@@ -22,11 +24,11 @@ fn is_loopback_or_stub(addr: &str) -> bool {
|
|||||||
pub struct ForwardingRule {
|
pub struct ForwardingRule {
|
||||||
pub suffix: String,
|
pub suffix: String,
|
||||||
dot_suffix: String, // pre-computed ".suffix" for zero-alloc matching
|
dot_suffix: String, // pre-computed ".suffix" for zero-alloc matching
|
||||||
pub upstream: SocketAddr,
|
pub upstream: Upstream,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForwardingRule {
|
impl ForwardingRule {
|
||||||
pub fn new(suffix: String, upstream: SocketAddr) -> Self {
|
pub fn new(suffix: String, upstream: Upstream) -> Self {
|
||||||
let dot_suffix = format!(".{}", suffix);
|
let dot_suffix = format!(".{}", suffix);
|
||||||
Self {
|
Self {
|
||||||
suffix,
|
suffix,
|
||||||
@@ -233,7 +235,7 @@ fn discover_macos() -> SystemDnsInfo {
|
|||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
fn make_rule(domain: &str, nameserver: &str) -> Option<ForwardingRule> {
|
fn make_rule(domain: &str, nameserver: &str) -> Option<ForwardingRule> {
|
||||||
let addr = crate::forward::parse_upstream_addr(nameserver, 53).ok()?;
|
let addr = crate::forward::parse_upstream_addr(nameserver, 53).ok()?;
|
||||||
Some(ForwardingRule::new(domain.to_string(), addr))
|
Some(ForwardingRule::new(domain.to_string(), Upstream::Udp(addr)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -822,10 +824,13 @@ fn uninstall_windows() -> Result<(), String> {
|
|||||||
/// Find the upstream for a domain by checking forwarding rules.
|
/// Find the upstream for a domain by checking forwarding rules.
|
||||||
/// Returns None if no rule matches (use default upstream).
|
/// Returns None if no rule matches (use default upstream).
|
||||||
/// Zero-allocation on the hot path — dot_suffix is pre-computed.
|
/// Zero-allocation on the hot path — dot_suffix is pre-computed.
|
||||||
pub fn match_forwarding_rule(domain: &str, rules: &[ForwardingRule]) -> Option<SocketAddr> {
|
pub fn match_forwarding_rule<'a>(
|
||||||
|
domain: &str,
|
||||||
|
rules: &'a [ForwardingRule],
|
||||||
|
) -> Option<&'a Upstream> {
|
||||||
for rule in rules {
|
for rule in rules {
|
||||||
if domain == rule.suffix || domain.ends_with(&rule.dot_suffix) {
|
if domain == rule.suffix || domain.ends_with(&rule.dot_suffix) {
|
||||||
return Some(rule.upstream);
|
return Some(&rule.upstream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|||||||
Reference in New Issue
Block a user