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))
from re import compile as Re
+import ipaddress
import os
import re
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