--- /dev/null
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+from collections.abc import AsyncGenerator
+
+import dns.rcode
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import (
+ ControllableAsyncDnsServer,
+ DnsResponseSend,
+ QueryContext,
+ ResponseHandler,
+)
+
+
+class DelayedAddressAnswerHandler(ResponseHandler):
+ async def get_responses(
+ self, qctx: QueryContext
+ ) -> AsyncGenerator[DnsResponseSend, None]:
+ if qctx.qtype in (dns.rdatatype.A, dns.rdatatype.AAAA):
+ addr = "192.0.2.1" if qctx.qtype == dns.rdatatype.A else "2001:db8:beef::1"
+ rrset = dns.rrset.from_text(qctx.qname, 300, qctx.qclass, qctx.qtype, addr)
+ qctx.response.answer.append(rrset)
+
+ delay = 0
+ if (
+ len(qctx.qname.labels) >= 2
+ and qctx.qname.labels[1] == b"latency"
+ and qctx.qname.labels[0].isdigit()
+ ):
+ delay = int(qctx.qname.labels[0]) / 1000
+ yield DnsResponseSend(qctx.response, delay=delay)
+
+
+def main() -> None:
+ server = ControllableAsyncDnsServer(
+ default_aa=True, default_rcode=dns.rcode.NOERROR
+ )
+ server.install_response_handler(DelayedAddressAnswerHandler())
+ server.run()
+
+
+if __name__ == "__main__":
+ main()
check_loaded(loaded, loaded_exp, now)
+def check_rtt(rtt, rtt_expected):
+ for val in rtt_expected:
+ assert rtt[val[0]] == val[1]
+
+
#
# The output is gibberish, but at least make sure it does not crash.
#
data = fetch_traffic(statsip, statsport)
check_traffic(data, exp)
+
+
+def test_rtt(fetch_views, **kwargs):
+ statsip = kwargs["statsip"]
+ statsport = kwargs["statsport"]
+
+ # auth query, 0 delay is expected, only for "in"
+ msg = create_msg("a.example2.", "TXT")
+ ans = isctest.query.tcp(msg, statsip, attempts=1)
+ isctest.check.noerror(ans)
+
+ # resolver query with a 530ms delay for both "in" and "out"
+ msg = create_msg("530.latency.example2.", "A")
+ ans = isctest.query.tcp(msg, statsip, attempts=1)
+ isctest.check.noerror(ans)
+
+ # resolver query with a 540ms delay for both "in" and "out"
+ msg = create_msg("540.latency.example2.", "A")
+ ans = isctest.query.tcp(msg, statsip, attempts=1)
+ isctest.check.noerror(ans)
+
+ # resolver query with a 730ms delay for both "in" and "out"
+ msg = create_msg("730.latency.example2.", "A")
+ ans = isctest.query.tcp(msg, statsip, attempts=1)
+ isctest.check.noerror(ans)
+
+ data = fetch_views(statsip, statsport)
+
+ check_rtt(data["in-queries-rtt"], [["~0", 1], ["512-575", 2], ["704-767", 1]])
+ check_rtt(data["out-queries-rtt"], [["512-575", 2], ["704-767", 1]])
--- /dev/null
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0. If a copy of the MPL was not distributed with this
+; file, you can obtain one at https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+$ORIGIN .
+$TTL 300 ; 5 minutes
+example2 IN SOA mname1. . (
+ 1 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+example2. NS ns4.example2.
+ns4.example2. A 10.53.0.4
+
+$ORIGIN example2.
+a A 10.0.0.1
+
+latency NS ns5.example2.
+ns5.example2. A 10.53.0.5
--- /dev/null
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+options {
+ query-source address 10.53.0.4;
+ notify-source 10.53.0.4;
+ transfer-source 10.53.0.4;
+ port @PORT@;
+ pid-file "named.pid";
+ listen-on { 10.53.0.4; };
+ listen-on-v6 { none; };
+ recursion yes;
+ dnssec-validation no;
+ notify no;
+ minimal-responses no;
+ version none; // make statistics independent of the version number
+};
+
+statistics-channels { inet 10.53.0.4 port @EXTRAPORT1@ allow { localhost; }; };
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+ inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "example2" {
+ type primary;
+ file "example2.db";
+ allow-transfer { any; };
+};
isctest.mark.with_json_c,
pytest.mark.extra_artifacts(
[
+ "ans5/ans.run",
"ns2/*.jnl",
"ns2/*.signed",
"ns2/dsset-*",
return data["traffic"]
+def fetch_rtt_json(statsip, statsport):
+ r = requests.get(f"http://{statsip}:{statsport}/json/v1", timeout=600)
+ assert r.status_code == 200
+
+ views = r.json()["views"]
+ data = {
+ "in-queries-rtt": views["_default"]["resolver"]["in-queries-rtt"],
+ "out-queries-rtt": views["_default"]["resolver"]["out-queries-rtt"],
+ }
+
+ return data
+
+
def load_timers_json(zone, primary=True):
name = zone["name"]
@pytest.mark.flaky(max_runs=2)
def test_traffic_json(statsport):
generic.test_traffic(fetch_traffic_json, statsip="10.53.0.2", statsport=statsport)
+
+
+@pytest.mark.flaky(max_runs=2)
+def test_rtt_json(statsport):
+ generic.test_rtt(fetch_rtt_json, statsip="10.53.0.4", statsport=statsport)
pytestmark = pytest.mark.extra_artifacts(
[
"K*",
+ "ans5/ans.run",
"bind9.xsl.1",
"bind9.xsl.2",
"compressed.headers",
pytest.mark.extra_artifacts(
[
"ns2/K*",
+ "ans5/ans.run",
"ns2/*.jnl",
"ns2/*.signed",
"ns2/dsset-*",
return traffic
+def fetch_rtt_xml(statsip, statsport):
+ def load_counters(data):
+ out = {}
+ for counter in data.findall("counter"):
+ out[counter.attrib["name"]] = int(counter.text)
+
+ return out
+
+ r = requests.get(f"http://{statsip}:{statsport}/xml/v3", timeout=600)
+ assert r.status_code == 200
+
+ root = ET.fromstring(r.text)
+
+ default_view = None
+ for view in root.find("views").iter("view"):
+ if view.attrib["name"] == "_default":
+ default_view = view
+ break
+ assert default_view is not None
+
+ rtt = {}
+ for counters in default_view.find("rtt").findall("counters"):
+ key = counters.attrib["type"]
+ values = load_counters(counters)
+ rtt[key] = values
+
+ return rtt
+
+
def load_timers_xml(zone, primary=True):
name = zone.attrib["name"]
@pytest.mark.flaky(max_runs=2)
def test_traffic_xml(statsport):
generic.test_traffic(fetch_traffic_xml, statsip="10.53.0.2", statsport=statsport)
+
+
+@pytest.mark.flaky(max_runs=2)
+def test_rtt_xml(statsport):
+ generic.test_rtt(fetch_rtt_xml, statsip="10.53.0.4", statsport=statsport)