From: Martin Basti Date: Wed, 22 Jul 2026 15:12:13 +0000 (+0000) Subject: Migrate digdelv dig +subnet checks to pytest X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=73f58fa448ba10479a69e7610a3f46d618fd0490;p=thirdparty%2Fbind9.git Migrate digdelv dig +subnet checks to pytest Move the dig EDNS client-subnet checks from tests.sh into tests_dig.py: +subnet with IPv4, IPv6 and zero-length subnets, the precedence of repeated +subnet options, prefix lengths 1-24 and prefixes between byte boundaries, raw +ednsopt=8 encodings rejected by the server, and the +yaml counterparts. The prefix-length loops are parametrized and the expected masked address is computed with the ipaddress module instead of a hardcoded table. Assisted-by: Claude:claude-fable-5 --- diff --git a/bin/tests/system/digdelv/tests.sh b/bin/tests/system/digdelv/tests.sh index 27f059a4f7..64220697f0 100644 --- a/bin/tests/system/digdelv/tests.sh +++ b/bin/tests/system/digdelv/tests.sh @@ -127,149 +127,6 @@ if [ -x "$DIG" ]; then echo_i "IPv6 unavailable; skipping" fi - n=$((n + 1)) - echo_i "checking dig +subnet ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.2 +subnet=127.0.0.1 A a.example >dig.out.test$n 2>&1 || ret=1 - grep "CLIENT-SUBNET: 127.0.0.1/32/0" /dev/null || ret=1 - check_ttl_range dig.out.test$n "A" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +subnet +subnet ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.2 +subnet=127.0.0.0 +subnet=127.0.0.1 A a.example >dig.out.test$n 2>&1 || ret=1 - grep "CLIENT-SUBNET: 127.0.0.1/32/0" /dev/null || ret=1 - check_ttl_range dig.out.test$n "A" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +subnet with various prefix lengths ($n)" - ret=0 - for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24; do - dig_with_opts +tcp @10.53.0.2 +subnet=255.255.255.255/$i A a.example >dig.out.$i.test$n 2>&1 || ret=1 - case $i in - 1 | 9 | 17) octet=128 ;; - 2 | 10 | 18) octet=192 ;; - 3 | 11 | 19) octet=224 ;; - 4 | 12 | 20) octet=240 ;; - 5 | 13 | 21) octet=248 ;; - 6 | 14 | 22) octet=252 ;; - 7 | 15 | 23) octet=254 ;; - 8 | 16 | 24) octet=255 ;; - esac - case $i in - 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8) addr="${octet}.0.0.0" ;; - 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16) addr="255.${octet}.0.0" ;; - 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24) addr="255.255.${octet}.0" ;; - esac - grep "FORMERR" /dev/null && ret=1 - grep "CLIENT-SUBNET: $addr/$i/0" /dev/null || ret=1 - check_ttl_range dig.out.$i.test$n "A" 300 || ret=1 - done - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +subnet=0/0 ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.2 +subnet=0/0 A a.example >dig.out.test$n 2>&1 || ret=1 - grep "status: NOERROR" /dev/null || ret=1 - grep "CLIENT-SUBNET: 0.0.0.0/0/0" /dev/null || ret=1 - grep "10.0.0.1" /dev/null || ret=1 - check_ttl_range dig.out.test$n "A" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +subnet=0 ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.2 +subnet=0 A a.example >dig.out.test$n 2>&1 || ret=1 - grep "status: NOERROR" /dev/null || ret=1 - grep "CLIENT-SUBNET: 0.0.0.0/0/0" /dev/null || ret=1 - grep "10.0.0.1" /dev/null || ret=1 - check_ttl_range dig.out.test$n "A" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - if [ $HAS_PYYAML -ne 0 ]; then - n=$((n + 1)) - echo_i "checking dig +subnet=0 +yaml ($n)" - ret=0 - dig_with_opts +yaml +tcp @10.53.0.2 +subnet=0 A a.example >dig.out.test$n 2>&1 || ret=1 - $PYTHON yamlget.py dig.out.test$n 0 message response_message_data OPT_PSEUDOSECTION EDNS CLIENT-SUBNET >yamlget.out.test$n 2>&1 || ret=1 - read -r value dig.out.test$n 2>&1 || ret=1 - grep "status: NOERROR" /dev/null || ret=1 - grep "CLIENT-SUBNET: ::/0/0" /dev/null || ret=1 - grep "10.0.0.1" /dev/null || ret=1 - check_ttl_range dig.out.test$n "A" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - if [ $HAS_PYYAML -ne 0 ]; then - n=$((n + 1)) - echo_i "checking dig +subnet=::/0 +yaml ($n)" - ret=0 - dig_with_opts +yaml +tcp @10.53.0.2 +subnet=::/0 A a.example >dig.out.test$n 2>&1 || ret=1 - $PYTHON yamlget.py dig.out.test$n 0 message response_message_data OPT_PSEUDOSECTION EDNS CLIENT-SUBNET >yamlget.out.test$n 2>&1 || ret=1 - read -r value dig.out.test$n 2>&1 || ret=1 - $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS CLIENT-SUBNET >yamlget.out.test$n 2>&1 || ret=1 - read -r value dig.out.test$n 2>&1 || ret=1 - grep "status: FORMERR" /dev/null || ret=1 - grep "CLIENT-SUBNET" /dev/null && ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +ednsopt=8:00030000 (family=3, source=0, scope=0) ($n)" - ret=0 - dig_with_opts +qr +tcp @10.53.0.2 +ednsopt=8:00030000 A a.example >dig.out.test$n 2>&1 || ret=1 - grep "status: FORMERR" /dev/null || ret=1 - grep "CLIENT-SUBNET: 00 03 00 00" /dev/null || ret=1 - test "$(grep -c "CLIENT-SUBNET: 00 03 00 00" dig.out.test$n)" -eq 1 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +subnet with prefix lengths between byte boundaries ($n)" - ret=0 - for p in 9 10 11 12 13 14 15; do - dig_with_opts +tcp @10.53.0.2 +subnet=10.53/$p A a.example >dig.out.test.$p.$n 2>&1 || ret=1 - grep "FORMERR" /dev/null && ret=1 - grep "CLIENT-SUBNET.*/$p/0" /dev/null || ret=1 - check_ttl_range dig.out.test.$p.$n "A" 300 || ret=1 - done - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - n=$((n + 1)) n=$((n + 1)) diff --git a/bin/tests/system/digdelv/tests_dig.py b/bin/tests/system/digdelv/tests_dig.py index e2868a4991..0ff03409c4 100644 --- a/bin/tests/system/digdelv/tests_dig.py +++ b/bin/tests/system/digdelv/tests_dig.py @@ -15,6 +15,7 @@ Tests for the dig tool. from re import compile as Re +import ipaddress import os import re @@ -687,3 +688,86 @@ def test_showtruncated(dig, ns2): result = dig(f"@{ns2.ip} +qr +showtruncated truncated.example TXT") assert Re(r"flags:[^;]* tc[ ;].*ANSWER: 0") in result.out assert "ANSWER: 100," in result.out + + +def test_subnet(dig, ns2): + """Check that dig +subnet sends the client subnet.""" + result = dig(f"+tcp @{ns2.ip} +subnet=127.0.0.1 A a.example") + assert "CLIENT-SUBNET: 127.0.0.1/32/0" in result.out + assert check_ttl_range(result.out, "A", 300) + + +def test_subnet_last_wins(dig, ns2): + """Check that the last of multiple +subnet options wins.""" + result = dig(f"+tcp @{ns2.ip} +subnet=127.0.0.0 +subnet=127.0.0.1 A a.example") + assert "CLIENT-SUBNET: 127.0.0.1/32/0" in result.out + assert check_ttl_range(result.out, "A", 300) + + +@pytest.mark.parametrize("plen", range(1, 25)) +def test_subnet_prefix_lengths(dig, ns2, plen): + """Check that dig +subnet masks the address to various prefix + lengths.""" + result = dig(f"+tcp @{ns2.ip} +subnet=255.255.255.255/{plen} A a.example") + addr = ipaddress.ip_address((0xFFFFFFFF << (32 - plen)) & 0xFFFFFFFF) + assert "FORMERR" not in result.out + assert f"CLIENT-SUBNET: {addr}/{plen}/0" in result.out + assert check_ttl_range(result.out, "A", 300) + + +@pytest.mark.parametrize("plen", range(9, 16)) +def test_subnet_prefix_between_byte_boundaries(dig, ns2, plen): + """Check dig +subnet with prefix lengths between byte boundaries.""" + result = dig(f"+tcp @{ns2.ip} +subnet=10.53/{plen} A a.example") + assert "FORMERR" not in result.out + assert Re(rf"CLIENT-SUBNET.*/{plen}/0") in result.out + assert check_ttl_range(result.out, "A", 300) + + +ZERO_SUBNETS = [ + param("+subnet=0/0", "0.0.0.0/0/0"), + param("+subnet=0", "0.0.0.0/0/0"), + param("+subnet=::/0", "::/0/0"), +] + + +@pytest.mark.parametrize("option,subnet", ZERO_SUBNETS) +def test_subnet_zero(dig, ns2, option, subnet): + """Check that a zero-length client subnet is sent and answered.""" + result = dig(f"+tcp @{ns2.ip} {option} A a.example") + assert "status: NOERROR" in result.out + assert f"CLIENT-SUBNET: {subnet}" in result.out + assert "10.0.0.1" in result.out + assert check_ttl_range(result.out, "A", 300) + + +@needs_pyyaml +@pytest.mark.parametrize("option,subnet", ZERO_SUBNETS) +def test_subnet_zero_yaml(dig, ns2, option, subnet): + """Check that a zero-length client subnet is echoed in the +yaml + response.""" + result = dig(f"+yaml +tcp @{ns2.ip} {option} A a.example") + assert edns_yaml(result.out, "response")["CLIENT-SUBNET"] == subnet + + +@needs_pyyaml +def test_subnet_yaml(dig, ns2): + """Check that +subnet=dead::/16 is shown in the +yaml query.""" + result = dig(f"+yaml +tcp @{ns2.ip} +qr +subnet=dead::/16 A a.example") + assert edns_yaml(result.out)["CLIENT-SUBNET"] == "dead::/16/0" + + +def test_subnet_raw_zero(dig, ns2): + """Check that a raw zero-length ECS option (family 0, source 0, + scope 0) is rejected by the server with FORMERR.""" + result = dig(f"+tcp @{ns2.ip} +ednsopt=8:00000000 A a.example") + assert "status: FORMERR" in result.out + assert "CLIENT-SUBNET" not in result.out + + +def test_subnet_raw_unknown_family(dig, ns2): + """Check that a raw ECS option with an unknown family (3) is sent + as-is and rejected by the server with FORMERR.""" + result = dig(f"+qr +tcp @{ns2.ip} +ednsopt=8:00030000 A a.example") + assert "status: FORMERR" in result.out + assert len(result.out.grep("CLIENT-SUBNET: 00 03 00 00")) == 1