mirror of
https://github.com/domainaware/parsedmarc.git
synced 2026-09-06 05:57:58 +00:00
Add DNS over HTTPS and DNS over TLS support via the nameservers option (#886)
Each entry in the existing nameservers option now selects its own transport (#880): an IP address means plain DNS on port 53 exactly as before, an https:// URL means DoH, and tls://ip[:port][#hostname] means DoT, with the optional #hostname naming the TLS certificate identity (systemd-resolved syntax). Forms can be mixed in one list, and no new configuration option is involved. DoH queries go through a shared per-process httpx client passed to dns.query.https as session=, which is what makes them honor HTTP_PROXY/ HTTPS_PROXY/NO_PROXY and SSL_CERT_FILE — the motivating proxy-only corporate network case. dnspython's stock DoH path cannot do this: it builds its httpx client around a custom transport, and httpx only reads proxy environment variables when no transport is supplied (allow_env_proxies = trust_env and transport is None). The client is rebuilt when the PID changes so fork-based worker pools never share a parent's sockets. The dnspython requirement becomes dnspython[doh]>=2.7.0 — the extra supplies the httpx/h2 floors DoH needs, and 2.7.0 is the floor verified against the dns.nameserver and dns.query.https(session=...) APIs used. The startup DNS pre-flight check now exercises whichever transports are configured, so a malformed DoH/DoT entry raises ConfigurationError before any mailbox work begins. Malformed tls:// entries — including the plausible slash-for-# typo tls://9.9.9.9/dns.quad9.net, which would otherwise silently drop the certificate identity — are rejected at configuration time naming the entry. Verified live: DoH A/PTR queries against Cloudflare and DoT against Quad9 (tls://9.9.9.9#dns.quad9.net), plus a full CLI run over a sample report with encrypted-DNS-only nameservers. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ffdb220af9
commit
cd864f4cfc
@@ -5747,6 +5747,59 @@ class TestParseConfigGeneral(unittest.TestCase):
|
||||
self.assertEqual(opts.dns_retries, 2)
|
||||
self.assertEqual(opts.nameservers, ["1.1.1.1", "8.8.8.8"])
|
||||
|
||||
def test_general_nameservers_accept_doh_and_dot_entries(self):
|
||||
"""A nameservers list may mix DNS over HTTPS URLs and DNS over TLS
|
||||
entries with plain IP addresses (issue #880); _parse_config splits
|
||||
and strips them like any other list value, and hands them to the DNS
|
||||
pre-flight check (mocked here so no network is needed)."""
|
||||
from parsedmarc.cli import _parse_config
|
||||
|
||||
cp = _config_with(
|
||||
"general",
|
||||
{
|
||||
"dns_test_address": "1.1.1.1",
|
||||
"dns_timeout": "5.0",
|
||||
"nameservers": (
|
||||
"https://cloudflare-dns.com/dns-query, tls://9.9.9.9#dns.quad9.net"
|
||||
),
|
||||
},
|
||||
)
|
||||
opts = _opts()
|
||||
with patch(
|
||||
"parsedmarc.cli.get_reverse_dns", return_value="one.one.one.one"
|
||||
) as mock_reverse_dns:
|
||||
_parse_config(cp, opts)
|
||||
self.assertEqual(
|
||||
opts.nameservers,
|
||||
["https://cloudflare-dns.com/dns-query", "tls://9.9.9.9#dns.quad9.net"],
|
||||
)
|
||||
self.assertEqual(
|
||||
mock_reverse_dns.call_args.kwargs["nameservers"], opts.nameservers
|
||||
)
|
||||
|
||||
def test_general_nameservers_malformed_dot_entry_fails_pre_flight(self):
|
||||
"""A malformed DNS over TLS nameservers entry raises a
|
||||
ConfigurationError at startup, before any mailbox work begins. No
|
||||
mocking is needed: the entry mapper rejects tls://not-an-ip with a
|
||||
ValueError before any query is sent (the pre-flight path passes no
|
||||
cache, so nothing can short-circuit it), and _parse_config wraps
|
||||
any pre-flight failure in a ConfigurationError."""
|
||||
from parsedmarc.cli import ConfigurationError, _parse_config
|
||||
|
||||
cp = _config_with(
|
||||
"general",
|
||||
{
|
||||
"dns_test_address": "1.1.1.1",
|
||||
"dns_timeout": "2.0",
|
||||
"nameservers": "tls://not-an-ip",
|
||||
},
|
||||
)
|
||||
opts = _opts()
|
||||
with self.assertRaises(ConfigurationError) as ctx:
|
||||
_parse_config(cp, opts)
|
||||
self.assertIn("pre-flight", str(ctx.exception))
|
||||
self.assertIn("tls://not-an-ip", str(ctx.exception))
|
||||
|
||||
def test_general_normalize_timespan_threshold(self):
|
||||
from parsedmarc.cli import _parse_config
|
||||
|
||||
|
||||
Reference in New Issue
Block a user