Skip to content

Commit

Permalink
enable proxy support for doq and doh3, fixes #96
Browse files Browse the repository at this point in the history
  • Loading branch information
tykling committed Jan 25, 2025
1 parent e8fdb51 commit 8a69c07
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 49 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- DNS-over-HTTP3 support: New protocol ``doh3`` was added for doing DNS-over-QUIC wrapped in HTTP3, aka DNS-over-HTTP3.
- Digestabot introduced to keep docker image digests up-to-date.
- Proxy support for DoQ and DoH3. Issue #96.

### Changed
- Bump dnspython dependency minimum version to 2.7.0
Expand Down
5 changes: 4 additions & 1 deletion src/dns_exporter/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ def __init__(
addr=self.config.proxy.hostname,
port=self.config.proxy.port,
)
dns.query.socket_factory = socks.socksocket
if self.config.protocol in ["doh3", "doq"]:
dns.quic._sync.socket_factory = socks.socksocket # noqa: SLF001
else:
dns.query.socket_factory = socks.socksocket
logger.debug(f"Using proxy {self.config.proxy.geturl()}")
else:
dns.query.socket_factory = socket.socket
Expand Down
4 changes: 1 addition & 3 deletions src/dns_exporter/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,9 @@ def validate_valid_rcodes(self) -> None:

def validate_proxy(self) -> None:
"""Validate proxy."""
if self.proxy and self.protocol in ["dot", "doq"]:
if self.proxy and self.protocol in ["dot"]:
# proxy support doesn't work for DoT for now
# https://github.com/tykling/dns_exporter/issues/76
# and doesn't work for DoQ until next dnspython release
# https://github.com/tykling/dns_exporter/issues/96
logger.error(f"proxy not valid for protocol {self.protocol}")
raise ConfigError(
"invalid_request_config",
Expand Down
8 changes: 6 additions & 2 deletions src/docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ The default value is ``udp``.


``proxy``
~~~~~~~~~~~~
~~~~~~~~~
This setting decides which proxy server to use, if any. The proxy must be provided including protocol, but port can be omitted if the default is fine. Hostname or IP can be used. Proxy protocol must be one of:

``SOCKS4``
Expand All @@ -190,10 +190,11 @@ This setting decides which proxy server to use, if any. The proxy must be provid
``HTTP``
HTTP proxy URL, for example ``http://example.com:8080`` - defaults to port ``8080``.

Using a proxy server is currently supported for protocols ``udp``, ``tcp``, and ``doh``. Support for protocol ``doq`` is coming with the next release of dnspython (see `this issue <https://github.com/rthalley/dnspython/pull/1060>`_). Support for protocol ``dot`` is planned for a later release.
Using a proxy server is currently supported for protocols ``udp``, ``tcp``, ``doh``, ``doh3``, and ``doq``. Support for protocol ``dot`` is planned for a later release.

.. Note:: Using a proxy server will affect DNS lookup measurements. When using a proxy the timing metrics are measuring both the time the actual DNS lookup takes as well as the roundtrip latency to the proxy server. As always when dealing with metrics consider carefully what you are measuring.


``query_class``
~~~~~~~~~~~~~~~
This setting decides the query class to use in the outgoing DNS query. Class ``IN`` and ``CHAOS`` are supported.
Expand Down Expand Up @@ -285,6 +286,7 @@ This setting has no default value.

.. Note:: The ``validate_authority_rrs`` setting can only be configured in a module in a config file. It can not be set in the scrape request querystring.


``validate_additional_rrs``
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This setting defines validation rules for the ``ADDITIONAL`` section of the DNS response. ``validate_additional_rrs`` can do the same validations as ``validate_answer_rrs``, see above for details.
Expand All @@ -293,6 +295,7 @@ This setting has no default value.

.. Note:: The ``validate_additional_rrs`` setting can only be configured in a module in a config file. It can not be set in the scrape request querystring.


``validate_response_flags``
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This setting can be used to validate the response flags of the DNS response. ``validate_response_flags`` can do the following validations:
Expand All @@ -313,6 +316,7 @@ This setting has no default value.

.. Note:: The ``validate_response_flags`` setting can only be configured in a module in a config file. It can not be set in the scrape request querystring.


``valid_rcodes``
~~~~~~~~~~~~~~~~
A comma seperated list of valid ``RCODE`` values. This setting defines the ``RCODE`` values to consider valid in the DNS response. The query is considered failed if the ``RCODE`` is not among the list in this setting.
Expand Down
8 changes: 4 additions & 4 deletions src/tests/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ def test_internal_metrics(dns_exporter_example_config, tmp_path, caplog):
assert f'dnsexp_build_version_info{{version="{__version__}"}} 1.0' in r.text
assert "Returning exporter metrics for request to /metrics" in caplog.text
for metric in """dnsexp_http_requests_total{path="/notfound"} 1.0
dnsexp_http_requests_total{path="/query"} 86.0
dnsexp_http_requests_total{path="/query"} 90.0
dnsexp_http_requests_total{path="/config"} 2.0
dnsexp_http_requests_total{path="/"} 1.0
dnsexp_http_requests_total{path="/metrics"} 1.0
dnsexp_http_responses_total{path="/notfound",response_code="404"} 1.0
dnsexp_http_responses_total{path="/query",response_code="200"} 86.0
dnsexp_http_responses_total{path="/query",response_code="200"} 90.0
dnsexp_http_responses_total{path="/",response_code="200"} 1.0
dnsexp_dns_queries_total 69.0
dnsexp_dns_responsetime_seconds_bucket{additional="0",answer="1",authority="0",family="ipv4",flags="QR RA RD",ip="8.8.4.4",le="2.5",nsid="no_nsid",opcode="QUERY",port="53",protocol="udp",proxy="none",query_name="example.com",query_type="A",rcode="NOERROR",server="udp://dns.google:53",transport="UDP"}
dnsexp_dns_queries_total 73.0
dnsexp_dns_responsetime_seconds_bucket{additional="0",answer="6",authority="0",family="ipv4",flags="QR RA RD",ip="8.8.4.4",le="2.5",nsid="no_nsid",opcode="QUERY",port="53",protocol="udp",proxy="none",query_name="example.com",query_type="A",rcode="NOERROR",server="udp://dns.google:53",transport="UDP"}
dnsexp_scrape_failures_total{additional="none",answer="none",authority="none",family="none",flags="none",ip="none",nsid="none",opcode="none",port="none",protocol="none",proxy="none",query_name="none",query_type="none",rcode="none",reason="invalid_request_config",server="none",transport="none"} 2.0
dnsexp_scrape_failures_total{additional="none",answer="none",authority="none",family="none",flags="none",ip="none",nsid="none",opcode="none",port="none",protocol="none",proxy="none",query_name="none",query_type="none",rcode="none",reason="invalid_request_server",server="none",transport="none"} 2.0
dnsexp_scrape_failures_total{additional="none",answer="none",authority="none",family="ipv6",flags="none",ip="192.0.2.42",nsid="none",opcode="none",port="420",protocol="udp",proxy="none",query_name="example.org",query_type="A",rcode="none",reason="timeout",server="udp://192.0.2.42:420",transport="none"} 2.0
Expand Down
56 changes: 17 additions & 39 deletions src/tests/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,60 +7,38 @@
###################################################################################


def test_proxy_udp(dns_exporter_example_config, proxy_server):
"""Test proxy functionality for udp protocol."""
@pytest.mark.parametrize(
("protocol", "server"),
[
("udp", "dns.google"),
("tcp", "dns.google"),
("doh", "anycast.uncensoreddns.org"),
("doh3", "anycast.uncensoreddns.org"),
("doq", "anycast.uncensoreddns.org"),
],
)
def test_proxy(dns_exporter_example_config, proxy_server, protocol, server):
"""Test proxy functionality for all protocols."""
r = requests.get(
"http://127.0.0.1:25353/query",
params={
"query_name": "example.com",
"server": "dns.google",
"server": server,
"family": "ipv4",
"protocol": "udp",
"proxy": "socks5://127.0.0.1:1080",
},
)
assert 'proxy="socks5://127.0.0.1:1080"' in r.text
assert 'server="udp://dns.google:53"' in r.text


def test_proxy_tcp(dns_exporter_example_config, proxy_server):
"""Test proxy functionality for tcp protocol."""
r = requests.get(
"http://127.0.0.1:25353/query",
params={
"query_name": "example.com",
"server": "dns.google",
"family": "ipv4",
"protocol": "tcp",
"proxy": "socks5://127.0.0.1:1080",
},
)
assert 'proxy="socks5://127.0.0.1:1080"' in r.text
assert 'server="tcp://dns.google:53"' in r.text


def test_proxy_doh(dns_exporter_example_config, proxy_server):
"""Test proxy functionality for doh protocol."""
r = requests.get(
"http://127.0.0.1:25353/query",
params={
"query_name": "example.com",
"server": "dns.google",
"family": "ipv4",
"protocol": "doh",
"protocol": protocol,
"proxy": "socks5://127.0.0.1:1080",
},
)
assert 'proxy="socks5://127.0.0.1:1080"' in r.text
assert 'server="doh://dns.google:443/dns-query"' in r.text
assert f'server="{protocol}://{server}:' in r.text


###################################################################################


@pytest.mark.parametrize("protocol", ["udp", "tcp", "doh"])
@pytest.mark.parametrize("protocol", ["udp", "tcp", "doh", "doh3", "doq"])
def test_proxy_fail(dns_exporter_example_config, proxy_server, protocol):
"""Test proxy failure for udp protocol."""
"""Test proxy failure for all protocols."""
r = requests.get(
"http://127.0.0.1:25353/query",
params={
Expand Down

0 comments on commit 8a69c07

Please sign in to comment.