Replace digdelv/ans8 with AsyncDnsServer

Previously, the ans8 server had different response modes that applied to
all queries. Replace it with AsyncDnsServer that has serves the different
response modes under different domains without the need to change the
server behaviour at runtime.

Add the new queries that require an ns3 fallback to the ns3/example.db
zone.
This commit is contained in:
Nicki Křížek 2025-11-28 16:41:44 +01:00
parent 575f0e3916
commit 0b7a089c7f
3 changed files with 59 additions and 192 deletions

View file

@ -9,194 +9,68 @@
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
from __future__ import print_function
import os
import sys
import signal
import socket
import select
import struct
from typing import AsyncGenerator
import dns, dns.message
from dns.rcode import *
import dns
import dns.rcode
modes = [
b"silent", # Do not respond
b"close", # UDP: same as silent; TCP: also close the connection
b"servfail", # Always respond with SERVFAIL
b"unstable", # Constantly switch between "silent" and "servfail"
]
mode = modes[0]
n = 0
from isctest.asyncserver import (
AsyncDnsServer,
CloseConnection,
DnsResponseSend,
DomainHandler,
IgnoreAllQueries,
QueryContext,
ResponseAction,
ResponseDrop,
)
def ctrl_channel(msg):
global modes, mode, n
class SilentHandler(DomainHandler, IgnoreAllQueries):
"""Handler that doesn't respond."""
msg = msg.splitlines().pop(0)
print("Received control message: %s" % msg)
if msg in modes:
mode = msg
n = 0
print("New mode: %s" % str(mode))
domains = ["silent.example"]
def create_servfail(msg):
m = dns.message.from_wire(msg)
qname = m.question[0].name.to_text()
rrtype = m.question[0].rdtype
typename = dns.rdatatype.to_text(rrtype)
class CloseHandler(DomainHandler):
"""Handler that doesn't respond and closes TCP connection."""
with open("query.log", "a") as f:
f.write("%s %s\n" % (typename, qname))
print("%s %s" % (typename, qname), end=" ")
domains = ["close.example"]
r = dns.message.make_response(m)
r.set_rcode(SERVFAIL)
return r
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[ResponseAction, None]:
yield CloseConnection()
def sigterm(signum, frame):
print("Shutting down now...")
os.remove("ans.pid")
running = False
sys.exit(0)
class SilentThenServfailHandler(DomainHandler):
"""Handler that drops one query and response to the next one with SERVFAIL."""
domains = ["silent-then-servfail.example"]
counter = 0
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[ResponseAction, None]:
if self.counter % 2 == 0:
yield ResponseDrop()
else:
qctx.response.set_rcode(dns.rcode.SERVFAIL)
yield DnsResponseSend(qctx.response, authoritative=False)
self.counter += 1
ip4 = "10.53.0.8"
def main() -> None:
server = AsyncDnsServer()
server.install_response_handlers(
[
CloseHandler(),
SilentHandler(),
SilentThenServfailHandler(),
]
)
server.run()
try:
port = int(os.environ["PORT"])
except:
port = 5300
try:
ctrlport = int(os.environ["EXTRAPORT1"])
except:
ctrlport = 5300
query4_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
query4_udp.bind((ip4, port))
query4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
query4_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
query4_tcp.bind((ip4, port))
query4_tcp.listen(100)
ctrl4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ctrl4_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ctrl4_tcp.bind((ip4, ctrlport))
ctrl4_tcp.listen(100)
signal.signal(signal.SIGTERM, sigterm)
f = open("ans.pid", "w")
pid = os.getpid()
print(pid, file=f)
f.close()
running = True
print("Listening on %s port %d" % (ip4, port))
print("Listening on %s port %d" % (ip4, ctrlport))
print("Ctrl-c to quit")
input = [query4_udp, query4_tcp, ctrl4_tcp]
hung_conns = []
while running:
try:
inputready, outputready, exceptready = select.select(input, [], [])
except select.error as e:
break
except socket.error as e:
break
except KeyboardInterrupt:
break
for s in inputready:
if s == query4_udp:
n = n + 1
print("UDP query received on %s" % ip4, end=" ")
msg = s.recvfrom(65535)
if (
mode == b"silent"
or mode == b"close"
or (mode == b"unstable" and n % 2 == 1)
):
# Do not respond.
print("NO RESPONSE (%s)" % str(mode))
continue
elif mode == b"servfail" or (mode == b"unstable" and n % 2 == 0):
rsp = create_servfail(msg[0])
if rsp:
print(dns.rcode.to_text(rsp.rcode()))
s.sendto(rsp.to_wire(), msg[1])
else:
print("NO RESPONSE (can not create a response)")
else:
raise (Exception("unsupported mode: %s" % mode))
elif s == query4_tcp:
n = n + 1
print("TCP query received on %s" % ip4, end=" ")
conn = None
try:
if mode == b"silent" or (mode == b"unstable" and n % 2 == 1):
conn, addr = s.accept()
# Do not respond and hang the connection.
print("NO RESPONSE (%s)" % str(mode))
hung_conns.append(conn)
continue
elif mode == b"close":
conn, addr = s.accept()
# Do not respond and close the connection.
print("NO RESPONSE (%s)" % str(mode))
conn.close()
continue
elif mode == b"servfail" or (mode == b"unstable" and n % 2 == 0):
conn, addr = s.accept()
# get TCP message length
msg = conn.recv(2)
if len(msg) != 2:
print("NO RESPONSE (can not read the message length)")
conn.close()
continue
length = struct.unpack(">H", msg[:2])[0]
msg = conn.recv(length)
if len(msg) != length:
print("NO RESPONSE (can not read the message)")
conn.close()
continue
rsp = create_servfail(msg)
if rsp:
print(dns.rcode.to_text(rsp.rcode()))
wire = rsp.to_wire()
conn.send(struct.pack(">H", len(wire)))
conn.send(wire)
else:
print("NO RESPONSE (can not create a response)")
else:
raise (Exception("unsupported mode: %s" % mode))
except socket.error as e:
print("NO RESPONSE (error: %s)" % str(e))
if conn:
conn.close()
elif s == ctrl4_tcp:
print("Control channel connected")
conn = None
try:
# Handle control channel input
conn, addr = s.accept()
msg = conn.recv(1024)
if msg:
ctrl_channel(msg)
conn.close()
except s.timeout:
pass
if conn:
conn.close()
if not running:
break
if __name__ == "__main__":
main()

View file

@ -33,6 +33,9 @@ c AAAA fd92:7065:b8e:ffff::3
d A 10.0.0.0
d AAAA fd92:7065:b8e:ffff::
silent A 10.0.0.1
close A 10.0.0.1
xn--caf-dma A 10.1.2.3
foo TXT "testing"

View file

@ -1275,20 +1275,16 @@ if [ -x "$DIG" ]; then
# See [GL #3020] for more information
n=$((n + 1))
echo_i "check that dig handles UDP timeout followed by a SERVFAIL correctly ($n)"
# Ask ans8 to be in "unstable" mode (switching between "silent" and "servfail" modes)
echo "unstable" | sendcmd 10.53.0.8
ret=0
dig_with_opts +timeout=1 +nofail @10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1
dig_with_opts +timeout=1 +nofail @10.53.0.8 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1
grep -F "status: SERVFAIL" 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 "check that dig handles TCP timeout followed by a SERVFAIL correctly ($n)"
# Ask ans8 to be in "unstable" mode (switching between "silent" and "servfail" modes)
echo "unstable" | sendcmd 10.53.0.8
ret=0
dig_with_opts +timeout=1 +nofail +tcp @10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1
dig_with_opts +timeout=1 +nofail +tcp @10.53.0.8 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1
grep -F "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1
if [ $ret -ne 0 ]; then echo_i "failed"; fi
status=$((status + ret))
@ -1321,10 +1317,8 @@ if [ -x "$DIG" ]; then
n=$((n + 1))
echo_i "check that dig tries the next server after a TCP socket read error ($n)"
# Ask ans8 to be in "close" mode, which closes the connection after accepting it
echo "close" | sendcmd 10.53.0.8
ret=0
dig_with_opts +tcp @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
dig_with_opts +tcp @10.53.0.8 @10.53.0.3 close.example >dig.out.test$n 2>&1 || ret=1
grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1
if [ $ret -ne 0 ]; then echo_i "failed"; fi
status=$((status + ret))
@ -1346,20 +1340,16 @@ if [ -x "$DIG" ]; then
n=$((n + 1))
echo_i "check that dig tries the next server after UDP socket read timeouts ($n)"
# Ask ans8 to be in "silent" mode
echo "silent" | sendcmd 10.53.0.8
ret=0
dig_with_opts +timeout=1 @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
dig_with_opts +timeout=1 @10.53.0.8 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1
grep -F "status: NOERROR" 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 "check that dig tries the next server after TCP socket read timeouts ($n)"
# Ask ans8 to be in "silent" mode
echo "silent" | sendcmd 10.53.0.8
ret=0
dig_with_opts +timeout=1 +tcp @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
dig_with_opts +timeout=1 +tcp @10.53.0.8 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1
grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1
if [ $ret -ne 0 ]; then echo_i "failed"; fi
status=$((status + ret))
@ -1368,7 +1358,7 @@ if [ -x "$DIG" ]; then
n=$((n + 1))
echo_i "check that dig correctly refuses to use a server with a IPv4 mapped IPv6 address after failing with a regular IP address ($n)"
ret=0
dig_with_opts @10.53.0.8 @::ffff:10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1
dig_with_opts @10.53.0.8 @::ffff:10.53.0.8 silent.example >dig.out.test$n 2>&1 || ret=1
grep -F ";; Skipping mapped address" dig.out.test$n >/dev/null || ret=1
grep -F ";; No acceptable nameservers" dig.out.test$n >/dev/null || ret=1
if [ $ret -ne 0 ]; then echo_i "failed"; fi