mirror of
https://github.com/isc-projects/bind9.git
synced 2026-06-10 16:59:59 -04:00
The counters update asynchronously, so a single snapshot can lag. Assisted-by: Claude:claude-opus-4-8
264 lines
8.3 KiB
Python
264 lines
8.3 KiB
Python
# 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 import defaultdict
|
|
from datetime import datetime, timedelta
|
|
from time import sleep
|
|
|
|
import os
|
|
|
|
import dns.message
|
|
|
|
import isctest
|
|
|
|
# ISO datetime format without msec
|
|
FMT = "%Y-%m-%dT%H:%M:%SZ"
|
|
|
|
# The constants were taken from BIND 9 source code (lib/dns/zone.c)
|
|
max_refresh = timedelta(seconds=2419200) # 4 weeks
|
|
max_expires = timedelta(seconds=14515200) # 24 weeks
|
|
dayzero = datetime.utcfromtimestamp(0).replace(microsecond=0)
|
|
|
|
# Wait for the secondary zone files to appear to extract their mtime
|
|
MAX_SECONDARY_ZONE_WAITTIME_SEC = 5
|
|
|
|
|
|
# Generic helper functions
|
|
def check_expires(expires, min_time, max_time):
|
|
assert expires >= min_time
|
|
assert expires <= max_time
|
|
|
|
|
|
def check_refresh(refresh, min_time, max_time):
|
|
assert refresh >= min_time
|
|
assert refresh <= max_time
|
|
|
|
|
|
def check_loaded(loaded, expected, now):
|
|
# Sanity check the zone timers values
|
|
assert (loaded - expected).total_seconds() < MAX_SECONDARY_ZONE_WAITTIME_SEC
|
|
assert loaded <= now
|
|
|
|
|
|
def check_zone_timers(loaded, expires, refresh, loaded_exp):
|
|
now = datetime.utcnow().replace(microsecond=0)
|
|
# Sanity checks the zone timers values
|
|
if expires is not None:
|
|
check_expires(expires, now, now + max_expires)
|
|
if refresh is not None:
|
|
check_refresh(refresh, now, now + max_refresh)
|
|
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.
|
|
#
|
|
def check_manykeys(name, zone=None):
|
|
# pylint: disable=unused-argument
|
|
assert name == "manykeys"
|
|
|
|
|
|
def zone_mtime(zonedir, name):
|
|
try:
|
|
si = os.stat(os.path.join(zonedir, f"{name}.db"))
|
|
except FileNotFoundError:
|
|
return dayzero
|
|
|
|
mtime = datetime.utcfromtimestamp(si.st_mtime).replace(microsecond=0)
|
|
|
|
return mtime
|
|
|
|
|
|
def test_zone_timers_primary(fetch_zones, load_timers, **kwargs):
|
|
statsip = kwargs["statsip"]
|
|
statsport = kwargs["statsport"]
|
|
zonedir = kwargs["zonedir"]
|
|
|
|
zones = fetch_zones(statsip, statsport)
|
|
|
|
for zone in zones:
|
|
name, loaded, expires, refresh = load_timers(zone, True)
|
|
mtime = zone_mtime(zonedir, name)
|
|
check_zone_timers(loaded, expires, refresh, mtime)
|
|
|
|
|
|
def test_zone_timers_secondary(fetch_zones, load_timers, **kwargs):
|
|
statsip = kwargs["statsip"]
|
|
statsport = kwargs["statsport"]
|
|
zonedir = kwargs["zonedir"]
|
|
|
|
# If any one of the zone files isn't ready, then retry until timeout.
|
|
tries = MAX_SECONDARY_ZONE_WAITTIME_SEC
|
|
while tries >= 0:
|
|
zones = fetch_zones(statsip, statsport)
|
|
again = False
|
|
for zone in zones:
|
|
name, loaded, expires, refresh = load_timers(zone, False)
|
|
mtime = zone_mtime(zonedir, name)
|
|
if (mtime != dayzero) or (tries == 0):
|
|
# mtime was either retrieved successfully or no tries were
|
|
# left, run the check anyway.
|
|
check_zone_timers(loaded, expires, refresh, mtime)
|
|
else:
|
|
tries = tries - 1
|
|
again = True
|
|
break
|
|
if again:
|
|
sleep(1)
|
|
else:
|
|
break
|
|
|
|
|
|
def test_zone_with_many_keys(fetch_zones, load_zone, **kwargs):
|
|
statsip = kwargs["statsip"]
|
|
statsport = kwargs["statsport"]
|
|
|
|
zones = fetch_zones(statsip, statsport)
|
|
|
|
for zone in zones:
|
|
name = load_zone(zone)
|
|
if name == "manykeys":
|
|
check_manykeys(name)
|
|
|
|
|
|
def create_msg(qname, qtype):
|
|
msg = dns.message.make_query(
|
|
qname, qtype, want_dnssec=True, use_edns=0, payload=4096
|
|
)
|
|
|
|
return msg
|
|
|
|
|
|
def create_expected(data):
|
|
expected = {
|
|
"dns-tcp-requests-sizes-received-ipv4": defaultdict(int),
|
|
"dns-tcp-responses-sizes-sent-ipv4": defaultdict(int),
|
|
"dns-tcp-requests-sizes-received-ipv6": defaultdict(int),
|
|
"dns-tcp-responses-sizes-sent-ipv6": defaultdict(int),
|
|
"dns-udp-requests-sizes-received-ipv4": defaultdict(int),
|
|
"dns-udp-requests-sizes-received-ipv6": defaultdict(int),
|
|
"dns-udp-responses-sizes-sent-ipv4": defaultdict(int),
|
|
"dns-udp-responses-sizes-sent-ipv6": defaultdict(int),
|
|
}
|
|
|
|
for k, v in data.items():
|
|
for kk, vv in v.items():
|
|
expected[k][kk] += vv
|
|
|
|
return expected
|
|
|
|
|
|
def update_expected(expected, key, msg):
|
|
msg_len = len(msg.to_wire())
|
|
bucket_num = (msg_len // 16) * 16
|
|
bucket = f"{bucket_num}-{bucket_num + 15}"
|
|
|
|
expected[key][bucket] += 1
|
|
|
|
|
|
def check_traffic(data, expected):
|
|
def ordered(obj):
|
|
if isinstance(obj, dict):
|
|
return sorted((k, ordered(v)) for k, v in obj.items())
|
|
if isinstance(obj, list):
|
|
return sorted(ordered(x) for x in obj)
|
|
return obj
|
|
|
|
ordered_data = ordered(data)
|
|
ordered_expected = ordered(expected)
|
|
|
|
assert len(ordered_data) == 8
|
|
assert len(ordered_expected) == 8
|
|
assert len(data) == len(ordered_data)
|
|
assert len(expected) == len(ordered_expected)
|
|
|
|
assert ordered_data == ordered_expected
|
|
|
|
|
|
def wait_for_traffic(fetch_traffic, statsip, statsport, expected):
|
|
# named updates the counters asynchronously, so a snapshot taken right
|
|
# after the query may lag; re-fetch until it matches (or time out).
|
|
def fetch_and_check():
|
|
check_traffic(fetch_traffic(statsip, statsport), expected)
|
|
return True
|
|
|
|
isctest.run.retry_with_timeout(fetch_and_check, timeout=10)
|
|
|
|
|
|
def test_traffic(fetch_traffic, **kwargs):
|
|
statsip = kwargs["statsip"]
|
|
statsport = kwargs["statsport"]
|
|
|
|
data = fetch_traffic(statsip, statsport)
|
|
exp = create_expected(data)
|
|
|
|
msg = create_msg("short.example.", "TXT")
|
|
update_expected(exp, "dns-udp-requests-sizes-received-ipv4", msg)
|
|
ans = isctest.query.udp(msg, statsip, attempts=1)
|
|
isctest.check.noerror(ans)
|
|
update_expected(exp, "dns-udp-responses-sizes-sent-ipv4", ans)
|
|
wait_for_traffic(fetch_traffic, statsip, statsport, exp)
|
|
|
|
msg = create_msg("long.example.", "TXT")
|
|
update_expected(exp, "dns-udp-requests-sizes-received-ipv4", msg)
|
|
ans = isctest.query.udp(msg, statsip, attempts=1)
|
|
isctest.check.noerror(ans)
|
|
update_expected(exp, "dns-udp-responses-sizes-sent-ipv4", ans)
|
|
wait_for_traffic(fetch_traffic, statsip, statsport, exp)
|
|
|
|
msg = create_msg("short.example.", "TXT")
|
|
update_expected(exp, "dns-tcp-requests-sizes-received-ipv4", msg)
|
|
ans = isctest.query.tcp(msg, statsip, attempts=1)
|
|
isctest.check.noerror(ans)
|
|
update_expected(exp, "dns-tcp-responses-sizes-sent-ipv4", ans)
|
|
wait_for_traffic(fetch_traffic, statsip, statsport, exp)
|
|
|
|
msg = create_msg("long.example.", "TXT")
|
|
update_expected(exp, "dns-tcp-requests-sizes-received-ipv4", msg)
|
|
ans = isctest.query.tcp(msg, statsip, attempts=1)
|
|
isctest.check.noerror(ans)
|
|
update_expected(exp, "dns-tcp-responses-sizes-sent-ipv4", ans)
|
|
wait_for_traffic(fetch_traffic, statsip, statsport, 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]])
|