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

Merged
razvandimescu merged 4 commits from feat/config-forwarding-rules into main 2026-04-12 11:12:08 +08:00
razvandimescu commented 2026-04-12 08:04:22 +08:00 (Migrated from github.com)

Summary

  • Adds [[forwarding]] config section for per-suffix conditional forwarding rules. Users can explicitly route domain suffixes (including reverse-DNS zones like 168.192.in-addr.arpa) to specific upstreams.
  • suffix accepts a string or array of strings, so multiple suffixes can share one upstream without repeating [[forwarding]] blocks.
  • Config-declared rules take precedence over auto-discovered rules (macOS scutil --dns, Linux search domains) via first-match semantics in match_forwarding_rule.
  • Bare IP upstreams default to port 53; IPv6 is supported via parse_upstream_addr.
  • Migrated make_rule() to use ForwardingRule::new() constructor and parse_upstream_addr, fixing a latent IPv6 parsing bug where bare ::1 would produce invalid ::1:53 instead of [::1]:53.

Test plan

  • make all — fmt, clippy (-D warnings), audit, build, 222 tests (12 new forwarding tests)
  • Config parsing: empty default, single rule, multiple rules, reverse-DNS zone, suffix array
  • Runtime conversion: explicit port, port-53 default, invalid upstream error, array expansion
  • Merge precedence: config rules win over discovered; non-overlapping discovered rules preserved; array expands config_count correctly
  • TDD workflow: tests written first, then implemented to green

Closes #82.

## Summary - Adds `[[forwarding]]` config section for per-suffix conditional forwarding rules. Users can explicitly route domain suffixes (including reverse-DNS zones like `168.192.in-addr.arpa`) to specific upstreams. - `suffix` accepts a string or array of strings, so multiple suffixes can share one upstream without repeating `[[forwarding]]` blocks. - Config-declared rules take precedence over auto-discovered rules (macOS `scutil --dns`, Linux search domains) via first-match semantics in `match_forwarding_rule`. - Bare IP upstreams default to port 53; IPv6 is supported via `parse_upstream_addr`. - Migrated `make_rule()` to use `ForwardingRule::new()` constructor and `parse_upstream_addr`, fixing a latent IPv6 parsing bug where bare `::1` would produce invalid `::1:53` instead of `[::1]:53`. ## Test plan - [x] `make all` — fmt, clippy (`-D warnings`), audit, build, 222 tests (12 new forwarding tests) - [x] Config parsing: empty default, single rule, multiple rules, reverse-DNS zone, suffix array - [x] Runtime conversion: explicit port, port-53 default, invalid upstream error, array expansion - [x] Merge precedence: config rules win over discovered; non-overlapping discovered rules preserved; array expands config_count correctly - [x] TDD workflow: tests written first, then implemented to green Closes #82.
Sign in to join this conversation.