]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Migrate digdelv dig +subnet checks to pytest
authorMartin Basti <mbasti@isc.org>
Wed, 22 Jul 2026 15:12:13 +0000 (15:12 +0000)
committerMartin Basti <mbasti@isc.org>
Wed, 29 Jul 2026 11:54:10 +0000 (13:54 +0200)
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
bin/tests/system/digdelv/tests.sh
bin/tests/system/digdelv/tests_dig.py

index 27f059a4f7a8769f3439d5c0f5068f2a46022cf9..64220697f0035602d4aba4dc91f3e3cabd100bad 100644 (file)
@@ -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" <dig.out.test$n >/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" <dig.out.test$n >/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" <dig.out.$i.test$n >/dev/null && ret=1
-    grep "CLIENT-SUBNET: $addr/$i/0" <dig.out.$i.test$n >/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" <dig.out.test$n >/dev/null || ret=1
-  grep "CLIENT-SUBNET: 0.0.0.0/0/0" <dig.out.test$n >/dev/null || ret=1
-  grep "10.0.0.1" <dig.out.test$n >/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" <dig.out.test$n >/dev/null || ret=1
-  grep "CLIENT-SUBNET: 0.0.0.0/0/0" <dig.out.test$n >/dev/null || ret=1
-  grep "10.0.0.1" <dig.out.test$n >/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 <yamlget.out.test$n
-    [ "$value" = "0.0.0.0/0/0" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  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" <dig.out.test$n >/dev/null || ret=1
-  grep "CLIENT-SUBNET: ::/0/0" <dig.out.test$n >/dev/null || ret=1
-  grep "10.0.0.1" <dig.out.test$n >/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 <yamlget.out.test$n
-    [ "$value" = "::/0/0" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-
-    n=$((n + 1))
-    echo_i "checking dig +subnet=dead::/16 +yaml ($n)"
-    ret=0
-    dig_with_opts +yaml +tcp @10.53.0.2 +qr +subnet=dead::/16 A a.example >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 <yamlget.out.test$n
-    [ "$value" = "dead::/16/0" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "checking dig +ednsopt=8:00000000 (family=0, source=0, scope=0) ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.2 +ednsopt=8:00000000 A a.example >dig.out.test$n 2>&1 || ret=1
-  grep "status: FORMERR" <dig.out.test$n >/dev/null || ret=1
-  grep "CLIENT-SUBNET" <dig.out.test$n >/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" <dig.out.test$n >/dev/null || ret=1
-  grep "CLIENT-SUBNET: 00 03 00 00" <dig.out.test$n >/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" <dig.out.test.$p.$n >/dev/null && ret=1
-    grep "CLIENT-SUBNET.*/$p/0" <dig.out.test.$p.$n >/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))
index e2868a499168da5c86e3398ef3123fbefdd2f466..0ff03409c4401a3f180bebcf199865fa16440193 100644 (file)
@@ -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