mirror of
https://gitlab.isc.org/isc-projects/bind9
synced 2025-08-30 22:15:20 +00:00
Add hypothesis strategies for generating DNS names and company
The most important being `dns_names` that generates dns.name.Name objects based on given paramaters. No guarantees are given when it comes the uniformity of generated samples, however it plays nicely with the hypothesis' shrinking algorithm. Once we use hypothesis more widely (in at least one more test) this file should be moved for it to be reused easily.
This commit is contained in:
committed by
Petr Špaček
parent
e7d46ad8ba
commit
5d738cd9ed
165
bin/tests/system/wildcard/strategies.py
Normal file
165
bin/tests/system/wildcard/strategies.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# 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 typing import List
|
||||
from warnings import warn
|
||||
|
||||
from hypothesis.strategies import (
|
||||
binary,
|
||||
builds,
|
||||
composite,
|
||||
integers,
|
||||
just,
|
||||
nothing,
|
||||
permutations,
|
||||
)
|
||||
|
||||
import dns.name
|
||||
import dns.message
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
|
||||
# LATER: Move this file so it can be easily reused.
|
||||
|
||||
|
||||
@composite
|
||||
def dns_names(
|
||||
draw,
|
||||
*,
|
||||
prefix: dns.name.Name = dns.name.empty,
|
||||
suffix: dns.name.Name = dns.name.root,
|
||||
min_labels: int = 1,
|
||||
max_labels: int = 128,
|
||||
) -> dns.name.Name:
|
||||
"""
|
||||
This is a hypothesis strategy to be used for generating DNS names with given `prefix`, `suffix`
|
||||
and with total number of labels specified by `min_labels` and `max labels`.
|
||||
|
||||
For example, calling
|
||||
```
|
||||
dns_names(
|
||||
prefix=dns.name.from_text("test"),
|
||||
suffix=dns.name.from_text("isc.org"),
|
||||
max_labels=6
|
||||
).example()
|
||||
```
|
||||
will result in names like `test.abc.isc.org.` or `test.abc.def.isc.org`.
|
||||
|
||||
There is no attempt to make the distribution of the generated names uniform in any way.
|
||||
The strategy however minimizes towards shorter names with shorter labels.
|
||||
|
||||
It can be used with to build compound strategies, like this one which generates random DNS queries.
|
||||
|
||||
```
|
||||
dns_queries = builds(
|
||||
dns.message.make_query,
|
||||
qname=dns_names(),
|
||||
rdtype=dns_rdatatypes,
|
||||
rdclass=dns_rdataclasses,
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
prefix = prefix.relativize(dns.name.root)
|
||||
suffix = suffix.derelativize(dns.name.root)
|
||||
|
||||
try:
|
||||
outer_name = prefix + suffix
|
||||
remaining_bytes = 255 - len(outer_name.to_wire())
|
||||
assert remaining_bytes >= 0
|
||||
except dns.name.NameTooLong:
|
||||
warn(
|
||||
"Maximal length name of name execeeded by prefix and suffix. Strategy won't generate any names.",
|
||||
RuntimeWarning,
|
||||
)
|
||||
return draw(nothing())
|
||||
|
||||
minimum_number_of_labels_to_generate = max(0, min_labels - len(outer_name.labels))
|
||||
maximum_number_of_labels_to_generate = max_labels - len(outer_name.labels)
|
||||
if maximum_number_of_labels_to_generate < 0:
|
||||
warn(
|
||||
"Maximal number of labels execeeded by prefix and suffix. Strategy won't generate any names.",
|
||||
RuntimeWarning,
|
||||
)
|
||||
return draw(nothing())
|
||||
|
||||
maximum_number_of_labels_to_generate = min(
|
||||
maximum_number_of_labels_to_generate, remaining_bytes // 2
|
||||
)
|
||||
if maximum_number_of_labels_to_generate < minimum_number_of_labels_to_generate:
|
||||
warn(
|
||||
f"Minimal number set to {minimum_number_of_labels_to_generate}, but in {remaining_bytes} bytes there is only space for maximum of {maximum_number_of_labels_to_generate} labels.",
|
||||
RuntimeWarning,
|
||||
)
|
||||
return draw(nothing())
|
||||
|
||||
if remaining_bytes == 0 or maximum_number_of_labels_to_generate == 0:
|
||||
warn(
|
||||
f"Strategy will return only one name ({outer_name}) as it exactly matches byte or label length limit.",
|
||||
RuntimeWarning,
|
||||
)
|
||||
return draw(just(outer_name))
|
||||
|
||||
chosen_number_of_labels_to_generate = draw(
|
||||
integers(
|
||||
minimum_number_of_labels_to_generate, maximum_number_of_labels_to_generate
|
||||
)
|
||||
)
|
||||
chosen_number_of_bytes_to_partion = draw(
|
||||
integers(2 * chosen_number_of_labels_to_generate, remaining_bytes)
|
||||
)
|
||||
chosen_lengths_of_labels = draw(
|
||||
_partition_bytes_to_labels(
|
||||
chosen_number_of_bytes_to_partion, chosen_number_of_labels_to_generate
|
||||
)
|
||||
)
|
||||
generated_labels = tuple(
|
||||
draw(binary(min_size=l - 1, max_size=l - 1)) for l in chosen_lengths_of_labels
|
||||
)
|
||||
|
||||
return dns.name.Name(prefix.labels + generated_labels + suffix.labels)
|
||||
|
||||
|
||||
RDATACLASS_MAX = RDATATYPE_MAX = 65535
|
||||
dns_rdataclasses = builds(dns.rdataclass.RdataClass, integers(0, RDATACLASS_MAX))
|
||||
dns_rdataclasses_without_meta = dns_rdataclasses.filter(dns.rdataclass.is_metaclass)
|
||||
dns_rdatatypes = builds(dns.rdatatype.RdataType, integers(0, RDATATYPE_MAX))
|
||||
|
||||
# NOTE: This should really be `dns_rdatatypes_without_meta = dns_rdatatypes_without_meta.filter(dns.rdatatype.is_metatype()`,
|
||||
# but hypothesis then complains about the filter being too strict, so it is done in a “constructive” way.
|
||||
dns_rdatatypes_without_meta = integers(0, dns.rdatatype.OPT - 1) | integers(dns.rdatatype.OPT + 1, 127) | integers(256, RDATATYPE_MAX) # type: ignore
|
||||
|
||||
|
||||
@composite
|
||||
def _partition_bytes_to_labels(
|
||||
draw, remaining_bytes: int, number_of_labels: int
|
||||
) -> List[int]:
|
||||
two_bytes_reserved_for_label = 2
|
||||
|
||||
# Reserve two bytes for each label
|
||||
partition = [two_bytes_reserved_for_label] * number_of_labels
|
||||
remaining_bytes -= two_bytes_reserved_for_label * number_of_labels
|
||||
|
||||
assert remaining_bytes >= 0
|
||||
|
||||
# Add a random number between 0 and the remainder to each partition
|
||||
for i in range(number_of_labels):
|
||||
added = draw(
|
||||
integers(0, min(remaining_bytes, 64 - two_bytes_reserved_for_label))
|
||||
)
|
||||
partition[i] += added
|
||||
remaining_bytes -= added
|
||||
|
||||
# NOTE: Some of the remaining bytes will usually not be assigned to any label, but we don't care.
|
||||
|
||||
return draw(permutations(partition))
|
Reference in New Issue
Block a user