mirror of
https://github.com/isc-projects/bind9.git
synced 2026-05-28 04:34:54 -04:00
Add regression test for NSEC proof after unsigned-to-signed IXFR
Test that a secondary receiving an IXFR transitioning a zone from unsigned to NSEC-signed returns the correct covering NSEC record for empty non-terminal names. Add isctest.query.wait_for_serial() shared helper for waiting until a server has a specific SOA serial.
This commit is contained in:
parent
245c71dfac
commit
8a4990d6ff
6 changed files with 225 additions and 0 deletions
|
|
@ -18,11 +18,14 @@ import time
|
|||
import dns.exception
|
||||
import dns.flags
|
||||
import dns.message
|
||||
import dns.name
|
||||
import dns.query
|
||||
import dns.rcode
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
|
||||
import isctest.log
|
||||
import isctest.run
|
||||
|
||||
QUERY_TIMEOUT = 10
|
||||
|
||||
|
|
@ -149,3 +152,33 @@ def create(
|
|||
if cd:
|
||||
msg.flags |= dns.flags.CD
|
||||
return msg
|
||||
|
||||
|
||||
def wait_for_serial(server_ip, zone, expected_serial, timeout=30):
|
||||
"""Wait until the server has the expected SOA serial for the zone.
|
||||
|
||||
Queries the server repeatedly until the SOA serial matches or the
|
||||
timeout expires.
|
||||
|
||||
'server_ip' is the IP address to query (string).
|
||||
'zone' is the zone name (string, with or without trailing dot).
|
||||
'expected_serial' is the expected SOA serial number (int).
|
||||
'timeout' is the maximum time to wait in seconds (default 30).
|
||||
"""
|
||||
query = create(zone, "SOA", dnssec=False)
|
||||
|
||||
def check():
|
||||
res = tcp(query, server_ip)
|
||||
soa = res.get_rrset(
|
||||
res.answer,
|
||||
dns.name.from_text(zone),
|
||||
dns.rdataclass.IN,
|
||||
dns.rdatatype.SOA,
|
||||
)
|
||||
return soa is not None and len(soa) == 1 and soa[0].serial == expected_serial
|
||||
|
||||
isctest.run.retry_with_timeout(
|
||||
check,
|
||||
timeout=timeout,
|
||||
msg=f"timed out waiting for serial {expected_serial} at {server_ip} for {zone}",
|
||||
)
|
||||
|
|
|
|||
11
bin/tests/system/nsec_ixfr/ns1/example.db.in
Normal file
11
bin/tests/system/nsec_ixfr/ns1/example.db.in
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
$TTL 300
|
||||
@ IN SOA ns1.example. hostmaster.example. (
|
||||
1 ; serial
|
||||
3600 ; refresh
|
||||
900 ; retry
|
||||
1209600 ; expire
|
||||
300 ; minimum
|
||||
)
|
||||
@ IN NS ns1.example.
|
||||
ns1 IN A 10.53.0.1
|
||||
*.wildcard IN A 10.0.0.1
|
||||
29
bin/tests/system/nsec_ixfr/ns1/named.conf.j2
Normal file
29
bin/tests/system/nsec_ixfr/ns1/named.conf.j2
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
key rndc_key {
|
||||
secret "1234abcd8765";
|
||||
algorithm @DEFAULT_HMAC@;
|
||||
};
|
||||
|
||||
controls {
|
||||
inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
|
||||
};
|
||||
|
||||
options {
|
||||
query-source address 10.53.0.1;
|
||||
notify-source 10.53.0.1;
|
||||
transfer-source 10.53.0.1;
|
||||
port @PORT@;
|
||||
pid-file "named.pid";
|
||||
listen-on { 10.53.0.1; };
|
||||
listen-on-v6 { none; };
|
||||
allow-transfer { any; };
|
||||
recursion no;
|
||||
notify yes;
|
||||
dnssec-validation no;
|
||||
};
|
||||
|
||||
zone "example" {
|
||||
type primary;
|
||||
file "example.db";
|
||||
also-notify { 10.53.0.2 port @PORT@; };
|
||||
ixfr-from-differences yes;
|
||||
};
|
||||
27
bin/tests/system/nsec_ixfr/ns2/named.conf.j2
Normal file
27
bin/tests/system/nsec_ixfr/ns2/named.conf.j2
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
key rndc_key {
|
||||
secret "1234abcd8765";
|
||||
algorithm @DEFAULT_HMAC@;
|
||||
};
|
||||
|
||||
controls {
|
||||
inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
|
||||
};
|
||||
|
||||
options {
|
||||
query-source address 10.53.0.2;
|
||||
notify-source 10.53.0.2;
|
||||
transfer-source 10.53.0.2;
|
||||
port @PORT@;
|
||||
pid-file "named.pid";
|
||||
listen-on { 10.53.0.2; };
|
||||
listen-on-v6 { none; };
|
||||
recursion no;
|
||||
notify yes;
|
||||
dnssec-validation no;
|
||||
};
|
||||
|
||||
zone "example" {
|
||||
type secondary;
|
||||
primaries { 10.53.0.1 port @PORT@; };
|
||||
file "example.db";
|
||||
};
|
||||
33
bin/tests/system/nsec_ixfr/setup.sh
Executable file
33
bin/tests/system/nsec_ixfr/setup.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/bin/sh
|
||||
|
||||
# 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.
|
||||
|
||||
# shellcheck source=conf.sh
|
||||
. ../conf.sh
|
||||
|
||||
set -e
|
||||
|
||||
# Start with the unsigned zone (serial 1).
|
||||
cp ns1/example.db.in ns1/example.db
|
||||
|
||||
# Generate DNSSEC keys for NSEC signing.
|
||||
"$KEYGEN" -q -f KSK -a "$DEFAULT_ALGORITHM" -K ns1 example >/dev/null
|
||||
"$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -K ns1 example >/dev/null
|
||||
|
||||
# Create the signed zone file with serial 2.
|
||||
# Bump the serial, then sign with plain NSEC (no -3 flag).
|
||||
# The -S flag tells dnssec-signzone to automatically find keys and
|
||||
# include DNSKEY records.
|
||||
sed 's/1\([ ]*;[ ]*serial\)/2\1/' ns1/example.db.in >ns1/example.db.tosign
|
||||
"$SIGNER" -P -S -K ns1 -o example -f ns1/example.db.signed \
|
||||
ns1/example.db.tosign >/dev/null 2>&1
|
||||
rm -f ns1/example.db.tosign
|
||||
92
bin/tests/system/nsec_ixfr/tests_nsec_ixfr.py
Normal file
92
bin/tests/system/nsec_ixfr/tests_nsec_ixfr.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#!/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.
|
||||
|
||||
"""Test that NSEC records received via IXFR produce correct denial-of-existence
|
||||
proofs for empty non-terminal names.
|
||||
|
||||
When a secondary receives NSEC records via IXFR (transitioning from an unsigned
|
||||
zone to an NSEC-signed zone), queries for empty non-terminal names should return
|
||||
the NSEC record that covers the ENT, not the zone apex NSEC."""
|
||||
|
||||
import shutil
|
||||
|
||||
import dns.name
|
||||
import dns.rdatatype
|
||||
|
||||
import isctest
|
||||
|
||||
ORIGIN = dns.name.from_text("example.")
|
||||
QNAME = dns.name.from_text("wildcard.example.")
|
||||
|
||||
|
||||
def test_nsec_ixfr_empty_nonterminal(ns1, ns2):
|
||||
"""Verify correct NSEC proof for ENT after IXFR from unsigned to signed.
|
||||
|
||||
1. Wait for ns2 to have the unsigned zone (serial 1) via AXFR.
|
||||
2. Switch ns1 to the signed zone (serial 2), reload.
|
||||
3. Wait for ns2 to pick up serial 2 (via IXFR).
|
||||
4. Query ns2 for wildcard.example. A +dnssec.
|
||||
5. Verify the AUTHORITY section contains the correct covering NSEC.
|
||||
"""
|
||||
|
||||
# Step 1: Wait for initial unsigned zone transfer to complete.
|
||||
isctest.query.wait_for_serial(ns2.ip, "example", 1)
|
||||
|
||||
# Step 2: Replace the zone on ns1 with the signed version and reload.
|
||||
shutil.copy("ns1/example.db.signed", "ns1/example.db")
|
||||
ns1.rndc("reload")
|
||||
|
||||
# Step 3: Wait for ns2 to get the signed zone via IXFR.
|
||||
isctest.query.wait_for_serial(ns2.ip, "example", 2)
|
||||
|
||||
# Step 4: Query ns2 for the empty non-terminal with DNSSEC.
|
||||
msg = isctest.query.create(QNAME, "A", dnssec=True)
|
||||
res = isctest.query.tcp(msg, ns2.ip)
|
||||
|
||||
# The ENT wildcard.example. has no A record, so this should be NOERROR
|
||||
# with an empty answer (the wildcard *.wildcard.example. does not match
|
||||
# wildcard.example. itself).
|
||||
isctest.check.noerror(res)
|
||||
assert len(res.answer) == 0, f"expected empty answer for ENT, got: {res.answer}"
|
||||
|
||||
# Step 5: Verify the NSEC record covers the ENT, not the apex.
|
||||
nsec_rrsets = [
|
||||
rrset for rrset in res.authority if rrset.rdtype == dns.rdatatype.NSEC
|
||||
]
|
||||
assert (
|
||||
len(nsec_rrsets) > 0
|
||||
), f"no NSEC records in authority section: {res.authority}"
|
||||
|
||||
# The bug (f4b4f030) returns the apex NSEC instead of the correct
|
||||
# covering NSEC, because the node's havensec flag was not set
|
||||
# during IXFR.
|
||||
for rrset in nsec_rrsets:
|
||||
assert rrset.name != ORIGIN, (
|
||||
f"got apex NSEC '{rrset.name} -> {rrset[0].next}' instead "
|
||||
f"of the covering NSEC for {QNAME}"
|
||||
)
|
||||
|
||||
# Verify the returned NSEC actually covers the ENT: the NSEC owner
|
||||
# must be canonically before the ENT, and the NSEC next name must be
|
||||
# canonically after (or wrap around to the apex).
|
||||
found_covering = False
|
||||
for rrset in nsec_rrsets:
|
||||
nsec_next = rrset[0].next
|
||||
if rrset.name < QNAME and (nsec_next > QNAME or nsec_next <= rrset.name):
|
||||
found_covering = True
|
||||
|
||||
assert (
|
||||
found_covering
|
||||
), f"no NSEC covers {QNAME}; " f"NSEC records found: " + ", ".join(
|
||||
f"'{rrset.name} -> {rrset[0].next}'" for rrset in nsec_rrsets
|
||||
)
|
||||
Loading…
Reference in a new issue