)
-class TcpOnlyHandler(ResponseHandler):
+class DropUdpHandler(ResponseHandler):
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[ResponseAction, None]:
- if qctx.protocol == DnsProtocol.TCP:
- yield DnsResponseSend(qctx.response)
- else:
+ if qctx.protocol == DnsProtocol.UDP:
yield ResponseDrop()
+ else:
+ yield DnsResponseSend(qctx.response)
def main() -> None:
server = AsyncDnsServer()
- server.install_response_handler(TcpOnlyHandler())
+ server.install_response_handler(DropUdpHandler())
server.run()
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
+from re import compile as Re
+from re import escape
+
import dns.message
+import dns.name
+import dns.rdataclass
+import dns.rdatatype
import pytest
import isctest
)
-def test_tcponly_not_resolved():
+def _count_received(path, qname, protocol):
+ pattern = Re(rf"Received {escape(qname)}/IN/A .* \({protocol}\)$")
+ with open(path, encoding="utf-8") as fh:
+ return sum(1 for line in fh if pattern.search(line.rstrip()))
+
+
+def test_tcponly_fallback():
"""
- An authoritative server that only answers over TCP is unreachable
- when its zone is queried over UDP: the resolver does not transparently
- fall back to TCP after UDP timeouts. (This confirms the expected behavior
- for this commit; TCP fallback will be restored in the next.)
+ A resolver must fall back to TCP after repeated UDP timeouts to the
+ same authoritative server. ans4 drops every UDP query and answers
+ only over TCP; the resolver must reach the answer via the TCP
+ fallback path, after at least two UDP attempts have been dropped.
"""
msg = dns.message.make_query("foo.tcp-only.", "A")
res = isctest.query.udp(msg, "10.53.0.2", timeout=15)
- isctest.check.servfail(res)
+ isctest.check.noerror(res)
+ rdataset = res.find_rrset(
+ res.answer,
+ dns.name.from_text("foo.tcp-only."),
+ dns.rdataclass.IN,
+ dns.rdatatype.A,
+ )
+ assert str(rdataset[0]) == "127.0.0.1"
+
+ udp = _count_received("ans4/ans.run", "foo.tcp-only", "UDP")
+ tcp = _count_received("ans4/ans.run", "foo.tcp-only", "TCP")
+ assert udp == 2, f"expected exactly 2 UDP queries, got {udp}"
+ assert tcp == 1, f"expected exactly 1 TCP query, got {tcp}"
isc_interval_set(&fctx->interval, seconds, us * NS_PER_US);
}
+static struct tried *
+triededns(fetchctx_t *fctx, isc_sockaddr_t *address);
+
static isc_result_t
fctx_query(fetchctx_t *fctx, dns_adbaddrinfo_t *addrinfo,
unsigned int options) {
}
}
+ /*
+ * If this server has already been tried at least twice in this
+ * fetch context after the previous attempt timed out, force TCP
+ * for this attempt. The decision must be made here, before the
+ * dispatch type is chosen below, so that the dispatch and the
+ * DNS_FETCHOPT_TCP flag agree.
+ */
+ if (fctx->timeout && fctx->timeouts >= 2U &&
+ (options & DNS_FETCHOPT_NOEDNS0) == 0 &&
+ (options & DNS_FETCHOPT_TCP) == 0)
+ {
+ struct tried *tried = triededns(fctx, &sockaddr);
+ if (tried != NULL && tried->count >= 2U) {
+ options |= DNS_FETCHOPT_TCP;
+ }
+ }
+
/*
* Allow an additional second for the kernel to resend the SYN
* (or SYN without ECN in the case of stupid firewalls blocking