feat: per-suffix conditional forwarding rules (#82) (#84)

* feat: per-suffix conditional forwarding rules in numa.toml (#82)

Adds a `[[forwarding]]` config section so users can explicitly route
domain suffixes to specific upstreams. Config-declared rules take
precedence over auto-discovered rules (macOS scutil, Linux search
domains) via first-match semantics.

Example — the reporter's reverse-DNS case:

  [[forwarding]]
  suffix = "168.192.in-addr.arpa"
  upstream = "100.90.1.63:5361"

Bare IPs default to port 53. IPv6 is supported via
parse_upstream_addr. ForwardingRule::new() constructor replaces
direct struct-literal construction, and make_rule() now delegates
to parse_upstream_addr to fix a latent IPv6 parsing bug.

* feat: accept suffix as string or array in [[forwarding]] rules

Reuses existing string_or_vec deserializer so users can write:
  suffix = ["168.192.in-addr.arpa", "onsite"]
instead of repeating [[forwarding]] blocks per suffix.

* style: rustfmt

* refactor: drop config_count from merge_forwarding_rules return

Log config rules directly from config.forwarding before merging,
keeping the merge API clean of logging concerns.
This commit was merged in pull request #84.
This commit is contained in:
Razvan Dimescu
2026-04-12 06:12:08 +03:00
committed by GitHub
parent 22bebb85a0
commit 7047767dc2
4 changed files with 212 additions and 7 deletions

View File

@@ -25,6 +25,17 @@ pub struct ForwardingRule {
pub upstream: SocketAddr,
}
impl ForwardingRule {
pub fn new(suffix: String, upstream: SocketAddr) -> Self {
let dot_suffix = format!(".{}", suffix);
Self {
suffix,
dot_suffix,
upstream,
}
}
}
/// Result of system DNS discovery — default upstream + conditional forwarding rules.
pub struct SystemDnsInfo {
pub default_upstream: Option<String>,
@@ -221,12 +232,8 @@ fn discover_macos() -> SystemDnsInfo {
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn make_rule(domain: &str, nameserver: &str) -> Option<ForwardingRule> {
let addr: SocketAddr = format!("{}:53", nameserver).parse().ok()?;
Some(ForwardingRule {
dot_suffix: format!(".{}", domain),
suffix: domain.to_string(),
upstream: addr,
})
let addr = crate::forward::parse_upstream_addr(nameserver, 53).ok()?;
Some(ForwardingRule::new(domain.to_string(), addr))
}
#[cfg(target_os = "linux")]