mirror of
https://github.com/isc-projects/bind9.git
synced 2026-05-21 09:26:12 -04:00
chg: usr: Fall back to TCP on a UDP response with a mismatched query id
BIND used to wait silently for the correct DNS message id on a UDP fetch even after receiving a response from the expected server with the wrong id, leaving room for off-path spoofing attempts to keep guessing within that window. The resolver now retries the fetch over TCP on the first such response, and a new MismatchTCP statistics counter tracks how often the fallback fires. Closes #5449 Merge branch '5449-immediate-tcp-fallback-on-id-mismatch' into 'main' See merge request isc-projects/bind9!12023
This commit is contained in:
commit
ef405bfa6d
12 changed files with 329 additions and 3 deletions
|
|
@ -539,6 +539,10 @@ init_desc(void) {
|
|||
SET_RESSTATDESC(priming, "priming queries", "Priming");
|
||||
SET_RESSTATDESC(forwardonlyfail, "all forwarders failed",
|
||||
"ForwardOnlyFail");
|
||||
SET_RESSTATDESC(mismatchtcp,
|
||||
"queries retried over TCP after a response with "
|
||||
"mismatched query id",
|
||||
"MismatchTCP");
|
||||
|
||||
INSIST(i == dns_resstatscounter_max);
|
||||
|
||||
|
|
|
|||
66
bin/tests/system/mismatchtcp/ans2/ans.py
Normal file
66
bin/tests/system/mismatchtcp/ans2/ans.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
"""
|
||||
Authoritative server that simulates Kaminsky-style off-path spoofing on UDP:
|
||||
for every UDP query for trigger.example./A it sends one response with a
|
||||
deliberately flipped DNS message id. A resolver that escalates to TCP on
|
||||
the first id mismatch will still get the correct answer over TCP, which
|
||||
this server serves normally.
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import dns.name
|
||||
import dns.rdatatype
|
||||
|
||||
from isctest.asyncserver import (
|
||||
AsyncDnsServer,
|
||||
DnsProtocol,
|
||||
DnsResponseSend,
|
||||
QueryContext,
|
||||
ResponseAction,
|
||||
ResponseHandler,
|
||||
)
|
||||
|
||||
|
||||
class MismatchOnUdpHandler(ResponseHandler):
|
||||
"""
|
||||
Spoof UDP queries for trigger.example./A with a properly-formed
|
||||
response whose DNS message id does not match the request. Answer
|
||||
the same query normally on TCP using the zone data prepared by the
|
||||
framework.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._trigger = dns.name.from_text("trigger.example.")
|
||||
|
||||
def match(self, qctx: QueryContext) -> bool:
|
||||
return qctx.qname == self._trigger and qctx.qtype == dns.rdatatype.A
|
||||
|
||||
async def get_responses(
|
||||
self, qctx: QueryContext
|
||||
) -> AsyncGenerator[ResponseAction, None]:
|
||||
if qctx.protocol == DnsProtocol.UDP:
|
||||
qctx.response.id = qctx.query.id ^ 0xFFFF
|
||||
yield DnsResponseSend(qctx.response)
|
||||
else:
|
||||
yield DnsResponseSend(qctx.response)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
server = AsyncDnsServer()
|
||||
server.install_response_handler(MismatchOnUdpHandler())
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
16
bin/tests/system/mismatchtcp/ans2/example.db
Normal file
16
bin/tests/system/mismatchtcp/ans2/example.db
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
;
|
||||
; SPDX-License-Identifier: MPL-2.0
|
||||
;
|
||||
; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
;
|
||||
; See the COPYRIGHT file distributed with this work for additional
|
||||
; information regarding copyright ownership.
|
||||
|
||||
$TTL 300
|
||||
example. SOA ns.example. . 0 0 0 0 0
|
||||
example. NS ns.example.
|
||||
ns.example. A 10.53.0.2
|
||||
trigger.example. A 192.0.2.42
|
||||
33
bin/tests/system/mismatchtcp/ns1/named.conf.j2
Normal file
33
bin/tests/system/mismatchtcp/ns1/named.conf.j2
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See the COPYRIGHT file distributed with this work for additional
|
||||
* information regarding copyright ownership.
|
||||
*/
|
||||
|
||||
include "../../_common/rndc.key";
|
||||
|
||||
controls {
|
||||
inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
|
||||
};
|
||||
|
||||
options {
|
||||
port @PORT@;
|
||||
pid-file "named.pid";
|
||||
listen-on { 10.53.0.1; };
|
||||
listen-on-v6 { none; };
|
||||
query-source address 10.53.0.1;
|
||||
recursion yes;
|
||||
dnssec-validation no;
|
||||
};
|
||||
|
||||
zone "." {
|
||||
type primary;
|
||||
file "root.db";
|
||||
};
|
||||
17
bin/tests/system/mismatchtcp/ns1/root.db
Normal file
17
bin/tests/system/mismatchtcp/ns1/root.db
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
;
|
||||
; SPDX-License-Identifier: MPL-2.0
|
||||
;
|
||||
; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
;
|
||||
; See the COPYRIGHT file distributed with this work for additional
|
||||
; information regarding copyright ownership.
|
||||
|
||||
$TTL 300
|
||||
. SOA . . 0 0 0 0 0
|
||||
. NS ns.nil.
|
||||
ns.nil. A 10.53.0.1
|
||||
example. NS ns.example.
|
||||
ns.example. A 10.53.0.2
|
||||
88
bin/tests/system/mismatchtcp/tests_mismatchtcp.py
Normal file
88
bin/tests/system/mismatchtcp/tests_mismatchtcp.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
"""
|
||||
End-to-end check for the immediate UDP-to-TCP fallback on a query-id
|
||||
mismatch.
|
||||
|
||||
The fake authoritative server at 10.53.0.2 answers every UDP query for
|
||||
trigger.example./A with a response whose DNS message id has been flipped.
|
||||
The resolver at 10.53.0.1 must escalate to TCP on the first such response
|
||||
and return the correct A record that the fake server serves over TCP.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import dns.message
|
||||
import dns.rdatatype
|
||||
import pytest
|
||||
|
||||
import isctest
|
||||
|
||||
pytestmark = pytest.mark.extra_artifacts(
|
||||
[
|
||||
"ans*/ans.run",
|
||||
"ns*/named.stats*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
MISMATCH_LABEL = "mismatch responses received"
|
||||
MISMATCHTCP_LABEL = "queries retried over TCP after a response with mismatched query id"
|
||||
|
||||
|
||||
def _named_stats(ns1) -> str:
|
||||
stats_path = Path(ns1.directory) / "named.stats"
|
||||
if stats_path.exists():
|
||||
stats_path.unlink()
|
||||
ns1.rndc("stats")
|
||||
return stats_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _counter(stats: str, label: str) -> int:
|
||||
for line in stats.splitlines():
|
||||
line = line.strip()
|
||||
if line.endswith(label):
|
||||
return int(line.split()[0])
|
||||
return 0
|
||||
|
||||
|
||||
def test_mismatch_tcp_fallback(ns1):
|
||||
"""
|
||||
Issue a single recursive query for a name whose UDP responses are
|
||||
being spoofed. The resolver must escalate to TCP on the first
|
||||
near-miss and return the correct A record.
|
||||
"""
|
||||
msg = dns.message.make_query("trigger.example.", dns.rdatatype.A, want_dnssec=False)
|
||||
res = isctest.query.udp(msg, ns1.ip, timeout=10)
|
||||
isctest.check.noerror(res)
|
||||
|
||||
answers = [rrset for rrset in res.answer if rrset.rdtype == dns.rdatatype.A]
|
||||
assert answers, f"no A RRset in response: {res}"
|
||||
addresses = {item.address for rrset in answers for item in rrset}
|
||||
assert "192.0.2.42" in addresses, f"unexpected answer: {addresses}"
|
||||
|
||||
|
||||
def test_mismatch_counter(ns1):
|
||||
"""
|
||||
After the spoofed exchange completes the resolver's existing
|
||||
"mismatch responses received" counter must be non-zero, confirming
|
||||
the dispatcher actually saw the wrong-id response, and the new
|
||||
"queries retried over TCP after a response with mismatched query
|
||||
id" counter must also be non-zero, confirming that the TCP
|
||||
fallback path actually fired in response to that mismatch.
|
||||
"""
|
||||
msg = dns.message.make_query("trigger.example.", dns.rdatatype.A, want_dnssec=False)
|
||||
isctest.query.udp(msg, ns1.ip, timeout=10)
|
||||
|
||||
stats = _named_stats(ns1)
|
||||
assert _counter(stats, MISMATCH_LABEL) > 0, stats
|
||||
assert _counter(stats, MISMATCHTCP_LABEL) > 0, stats
|
||||
|
|
@ -587,13 +587,18 @@ udp_recv(isc_nmhandle_t *handle, isc_result_t eresult, isc_region_t *region,
|
|||
}
|
||||
|
||||
/*
|
||||
* The QID and the address must match the expected ones.
|
||||
* The QID and the address must match the expected ones. A
|
||||
* mismatch can happen during normal operation only when a stale
|
||||
* response from a previous query arrives late, which is rare in
|
||||
* practice; treat any mismatch as a possible spoofing attempt and
|
||||
* let the caller retry over TCP to prevent off-path spoofing.
|
||||
*/
|
||||
if (resp->id != id || !isc_sockaddr_equal(&peer, &resp->peer)) {
|
||||
dispentry_log(resp, ISC_LOG_DEBUG(90),
|
||||
"response doesn't match");
|
||||
inc_stats(disp->mgr, dns_resstatscounter_mismatch);
|
||||
goto next;
|
||||
eresult = DNS_R_MISMATCH;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ enum {
|
|||
dns_resstatscounter_nextitem = 38,
|
||||
dns_resstatscounter_priming = 39,
|
||||
dns_resstatscounter_forwardonlyfail = 40,
|
||||
dns_resstatscounter_max = 41,
|
||||
dns_resstatscounter_mismatchtcp = 41,
|
||||
dns_resstatscounter_max = 42,
|
||||
|
||||
/*
|
||||
* DNSSEC stats.
|
||||
|
|
|
|||
|
|
@ -8084,6 +8084,22 @@ rctx_dispfail(respctx_t *rctx) {
|
|||
rctx->finish = NULL;
|
||||
rctx->no_response = true;
|
||||
break;
|
||||
case DNS_R_MISMATCH:
|
||||
/*
|
||||
* The dispatcher saw a UDP response from the expected peer with
|
||||
* the wrong DNS message id. Retry the same query over TCP.
|
||||
*/
|
||||
if ((rctx->retryopts & DNS_FETCHOPT_TCP) == 0) {
|
||||
rctx->retryopts |= DNS_FETCHOPT_TCP;
|
||||
rctx->resend = true;
|
||||
rctx->next_server = false;
|
||||
inc_stats(fctx->res, dns_resstatscounter_mismatchtcp);
|
||||
FCTXTRACE3("mismatched response; retrying over TCP",
|
||||
rctx->result);
|
||||
rctx_done(rctx, ISC_R_SUCCESS);
|
||||
return ISC_R_COMPLETE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ typedef enum isc_result {
|
|||
DNS_R_NOSKRBUNDLE,
|
||||
DNS_R_LOOPDETECTED,
|
||||
DNS_R_INVALIDDSYNC,
|
||||
DNS_R_MISMATCH,
|
||||
|
||||
DST_R_UNSUPPORTEDALG,
|
||||
DST_R_CRYPTOFAILURE,
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ static const char *description[ISC_R_NRESULTS] = {
|
|||
[DNS_R_NOSKRBUNDLE] = "no available SKR bundle",
|
||||
[DNS_R_LOOPDETECTED] = "fetch loop detected",
|
||||
[DNS_R_INVALIDDSYNC] = "invalid DSYNC response",
|
||||
[DNS_R_MISMATCH] = "response with mismatched query id",
|
||||
|
||||
[DST_R_UNSUPPORTEDALG] = "algorithm is unsupported",
|
||||
[DST_R_CRYPTOFAILURE] = "crypto failure",
|
||||
|
|
@ -448,6 +449,7 @@ static const char *identifier[ISC_R_NRESULTS] = {
|
|||
[DNS_R_NOSKRBUNDLE] = "DNS_R_NOSKRBUNDLE",
|
||||
[DNS_R_LOOPDETECTED] = "DNS_R_LOOPDETECTED",
|
||||
[DNS_R_INVALIDDSYNC] = "DNS_R_INVALIDDSYNC",
|
||||
[DNS_R_MISMATCH] = "DNS_R_MISMATCH",
|
||||
|
||||
[DST_R_UNSUPPORTEDALG] = "DST_R_UNSUPPORTEDALG",
|
||||
[DST_R_CRYPTOFAILURE] = "DST_R_CRYPTOFAILURE",
|
||||
|
|
|
|||
|
|
@ -445,6 +445,41 @@ response_shutdown(isc_result_t eresult, isc_region_t *region ISC_ATTR_UNUSED,
|
|||
test_dispatch_shutdown(test);
|
||||
}
|
||||
|
||||
static void
|
||||
response_mismatch(isc_result_t eresult, isc_region_t *region ISC_ATTR_UNUSED,
|
||||
void *arg) {
|
||||
test_dispatch_t *test = arg;
|
||||
|
||||
assert_int_equal(eresult, DNS_R_MISMATCH);
|
||||
|
||||
test_dispatch_shutdown(test);
|
||||
}
|
||||
|
||||
static void
|
||||
nameserver_mismatch(isc_nmhandle_t *handle, isc_result_t eresult,
|
||||
isc_region_t *region, void *arg ISC_ATTR_UNUSED) {
|
||||
/*
|
||||
* Reply with a single response whose DNS message id has been
|
||||
* flipped; the dispatcher must escalate immediately rather than
|
||||
* waiting for a "correct" id to arrive.
|
||||
*/
|
||||
static unsigned char buf[16];
|
||||
static isc_region_t resp;
|
||||
|
||||
if (eresult != ISC_R_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
memmove(buf, region->base, 12);
|
||||
memset(buf + 12, 0, 4);
|
||||
buf[0] ^= 0xff; /* flip id high byte */
|
||||
buf[1] ^= 0xff; /* flip id low byte */
|
||||
buf[2] |= 0x80; /* qr=1 */
|
||||
resp.base = buf;
|
||||
resp.length = sizeof(buf);
|
||||
isc_nm_send(handle, &resp, server_senddone, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
response_timeout(isc_result_t eresult, isc_region_t *region ISC_ATTR_UNUSED,
|
||||
void *arg) {
|
||||
|
|
@ -763,6 +798,47 @@ ISC_LOOP_TEST_IMPL(dispatch_getnext) {
|
|||
dns_dispatch_connect(test->dispentry);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify that a UDP response carrying the wrong DNS message id causes the
|
||||
* dispatcher to deliver DNS_R_MISMATCH to the response callback, instead
|
||||
* of silently waiting for the correct id to arrive.
|
||||
*/
|
||||
ISC_LOOP_TEST_IMPL(dispatch_mismatch_tcp) {
|
||||
isc_result_t result;
|
||||
test_dispatch_t *test = isc_mem_get(isc_g_mctx, sizeof(*test));
|
||||
*test = (test_dispatch_t){ 0 };
|
||||
|
||||
/* Server: replies with a single wrong-id response. */
|
||||
result = isc_nm_listenudp(ISC_NM_LISTEN_ONE, &udp_server_addr,
|
||||
nameserver_mismatch, NULL, &sock);
|
||||
assert_int_equal(result, ISC_R_SUCCESS);
|
||||
|
||||
isc_loop_teardown(isc_loop_main(), stop_listening, sock);
|
||||
|
||||
/* Client */
|
||||
testdata.region.base = testdata.message;
|
||||
testdata.region.length = sizeof(testdata.message);
|
||||
|
||||
result = dns_dispatchmgr_create(isc_g_mctx, &test->dispatchmgr);
|
||||
assert_int_equal(result, ISC_R_SUCCESS);
|
||||
|
||||
result = dns_dispatch_createudp(test->dispatchmgr, &udp_connect_addr,
|
||||
&test->dispatch);
|
||||
assert_int_equal(result, ISC_R_SUCCESS);
|
||||
|
||||
result = dns_dispatch_add(test->dispatch, isc_loop_main(), 0,
|
||||
T_CLIENT_CONNECT, T_CLIENT_INIT,
|
||||
&udp_server_addr, NULL, NULL, connected,
|
||||
client_senddone, response_mismatch, test,
|
||||
&test->id, &test->dispentry);
|
||||
assert_int_equal(result, ISC_R_SUCCESS);
|
||||
|
||||
testdata.message[0] = (test->id >> 8) & 0xff;
|
||||
testdata.message[1] = test->id & 0xff;
|
||||
|
||||
dns_dispatch_connect(test->dispentry);
|
||||
}
|
||||
|
||||
ISC_LOOP_TEST_IMPL(dispatch_sharedtcp) {
|
||||
isc_result_t result;
|
||||
test_dispatch_t *test = isc_mem_get(isc_g_mctx, sizeof(*test));
|
||||
|
|
@ -806,6 +882,7 @@ ISC_TEST_ENTRY_CUSTOM(dispatch_timeout_tcp_connect, setup_test, teardown_test)
|
|||
ISC_TEST_ENTRY_CUSTOM(dispatch_tcp_response, setup_test, teardown_test)
|
||||
ISC_TEST_ENTRY_CUSTOM(dispatch_tls_response, setup_test, teardown_test)
|
||||
ISC_TEST_ENTRY_CUSTOM(dispatch_getnext, setup_test, teardown_test)
|
||||
ISC_TEST_ENTRY_CUSTOM(dispatch_mismatch_tcp, setup_test, teardown_test)
|
||||
ISC_TEST_LIST_END
|
||||
|
||||
ISC_TEST_MAIN
|
||||
|
|
|
|||
Loading…
Reference in a new issue