mirror of
https://github.com/isc-projects/bind9.git
synced 2026-05-21 01:15:23 -04:00
Force TCP after repeated UDP timeouts to the same authoritative
Make the decision in fctx_query() before the dispatch is bound so the chosen transport and the DNS_FETCHOPT_TCP flag agree. The previous location in resquery_send() ran after the UDP dispatch had already been attached, so the flag flip had no effect on the wire. Moving the decision earlier also means FCTX_ADDRINFO_NOEDNS0 servers, previously exempt, now escalate to TCP too. TCP works regardless of EDNS state, so this is the intended behaviour. Assisted-by: Claude:claude-opus-4-7
This commit is contained in:
parent
01523a078a
commit
59c00a6f31
3 changed files with 55 additions and 11 deletions
|
|
@ -22,19 +22,19 @@ from isctest.asyncserver import (
|
|||
)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,13 @@
|
|||
# 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
|
||||
|
|
@ -21,13 +27,31 @@ pytestmark = pytest.mark.extra_artifacts(
|
|||
)
|
||||
|
||||
|
||||
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}"
|
||||
|
|
|
|||
|
|
@ -2033,6 +2033,9 @@ fctx_setretryinterval(fetchctx_t *fctx, unsigned int rtt) {
|
|||
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) {
|
||||
|
|
@ -2126,6 +2129,23 @@ fctx_query(fetchctx_t *fctx, dns_adbaddrinfo_t *addrinfo,
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue