2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-08-31 14:35:26 +00:00

Check that a zone that serves A/AAAA is served over IPv4/IPv6

named-checkzone will now, as part of the zone's integrity checks,
look to see if there are A or AAAA records being served and if so
check that the nameservers have A or AAAA records respectively.

These are a sometimes overlooked checks that, if not met, can mean
that a service that is supposed to reachable over IPv6 will not be
resolvable when the recursive resolver is IPv6 only.  Similarly for
IPv4 servers when there are IPv4 only resolvers.
This commit is contained in:
Mark Andrews
2023-10-17 14:49:14 +11:00
parent 3db39ec7ad
commit 6d44e7320e
5 changed files with 302 additions and 41 deletions

View File

@@ -144,12 +144,97 @@ logged(char *key, int value) {
return false;
}
static bool
checkisservedby(dns_zone_t *zone, dns_rdatatype_t type,
const dns_name_t *name) {
char namebuf[DNS_NAME_FORMATSIZE + 1];
char ownerbuf[DNS_NAME_FORMATSIZE + 1];
/*
* Not all getaddrinfo implementations distinguish NODATA
* from NXDOMAIN with PF_INET6 so use PF_UNSPEC and look at
* the returned ai_family values.
*/
struct addrinfo hints = {
.ai_flags = AI_CANONNAME,
.ai_family = PF_UNSPEC,
.ai_socktype = SOCK_STREAM,
.ai_protocol = IPPROTO_TCP,
};
struct addrinfo *ai = NULL, *cur;
bool has_type = false;
int eai;
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
/*
* Turn off search.
*/
if (dns_name_countlabels(name) > 1U) {
strlcat(namebuf, ".", sizeof(namebuf));
}
eai = getaddrinfo(namebuf, NULL, &hints, &ai);
switch (eai) {
case 0:
cur = ai;
while (cur != NULL) {
if (cur->ai_family == AF_INET &&
type == dns_rdatatype_a)
{
has_type = true;
break;
}
if (cur->ai_family == AF_INET6 &&
type == dns_rdatatype_aaaa)
{
has_type = true;
break;
}
cur = cur->ai_next;
}
freeaddrinfo(ai);
return has_type;
#if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME)
case EAI_NODATA:
#endif /* if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME) */
case EAI_NONAME:
if (!logged(namebuf, ERR_NO_ADDRESSES)) {
dns_name_format(dns_zone_getorigin(zone), ownerbuf,
sizeof(ownerbuf));
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
dns_zone_log(zone, ISC_LOG_ERROR,
"%s/NS '%s' (out of zone) "
"has no addresses records (A or AAAA)",
ownerbuf, namebuf);
add(namebuf, ERR_NO_ADDRESSES);
}
return false;
default:
if (!logged(namebuf, ERR_LOOKUP_FAILURE)) {
dns_name_format(dns_zone_getorigin(zone), ownerbuf,
sizeof(ownerbuf));
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
dns_zone_log(zone, ISC_LOG_WARNING,
"getaddrinfo(%s) failed: %s", namebuf,
gai_strerror(eai));
add(namebuf, ERR_LOOKUP_FAILURE);
}
return true;
}
}
static bool
checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
dns_rdataset_t *a, dns_rdataset_t *aaaa) {
dns_rdataset_t *rdataset;
dns_rdata_t rdata = DNS_RDATA_INIT;
struct addrinfo hints, *ai, *cur;
isc_result_t result;
struct addrinfo hints = {
.ai_flags = AI_CANONNAME,
.ai_family = PF_UNSPEC,
.ai_socktype = SOCK_STREAM,
.ai_protocol = IPPROTO_TCP,
};
struct addrinfo *ai = NULL, *cur;
char namebuf[DNS_NAME_FORMATSIZE + 1];
char ownerbuf[DNS_NAME_FORMATSIZE];
char addrbuf[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:123.123.123.123")];
@@ -157,7 +242,7 @@ checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
bool match;
const char *type;
void *ptr = NULL;
int result;
int eai;
REQUIRE(a == NULL || !dns_rdataset_isassociated(a) ||
a->type == dns_rdatatype_a);
@@ -168,12 +253,6 @@ checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
return answer;
}
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
/*
* Turn off search.
@@ -183,9 +262,9 @@ checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
}
dns_name_format(owner, ownerbuf, sizeof(ownerbuf));
result = getaddrinfo(namebuf, NULL, &hints, &ai);
eai = getaddrinfo(namebuf, NULL, &hints, &ai);
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
switch (result) {
switch (eai) {
case 0:
/*
* Work around broken getaddrinfo() implementations that
@@ -228,7 +307,7 @@ checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
if (!logged(namebuf, ERR_LOOKUP_FAILURE)) {
dns_zone_log(zone, ISC_LOG_WARNING,
"getaddrinfo(%s) failed: %s", namebuf,
gai_strerror(result));
gai_strerror(eai));
add(namebuf, ERR_LOOKUP_FAILURE);
}
return true;
@@ -358,25 +437,27 @@ checkmissing:
add(namebuf, ERR_MISSING_GLUE);
}
}
freeaddrinfo(ai);
if (ai != NULL) {
freeaddrinfo(ai);
}
return answer;
}
static bool
checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
struct addrinfo hints, *ai, *cur;
struct addrinfo hints = {
.ai_flags = AI_CANONNAME,
.ai_family = PF_UNSPEC,
.ai_socktype = SOCK_STREAM,
.ai_protocol = IPPROTO_TCP,
};
struct addrinfo *ai = NULL, *cur;
char namebuf[DNS_NAME_FORMATSIZE + 1];
char ownerbuf[DNS_NAME_FORMATSIZE];
int result;
int eai;
int level = ISC_LOG_ERROR;
bool answer = true;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
/*
* Turn off search.
@@ -386,9 +467,9 @@ checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
}
dns_name_format(owner, ownerbuf, sizeof(ownerbuf));
result = getaddrinfo(namebuf, NULL, &hints, &ai);
eai = getaddrinfo(namebuf, NULL, &hints, &ai);
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
switch (result) {
switch (eai) {
case 0:
/*
* Work around broken getaddrinfo() implementations that
@@ -421,7 +502,9 @@ checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
}
}
}
freeaddrinfo(ai);
if (ai != NULL) {
freeaddrinfo(ai);
}
return answer;
case EAI_NONAME:
@@ -442,7 +525,7 @@ checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
if (!logged(namebuf, ERR_LOOKUP_FAILURE)) {
dns_zone_log(zone, ISC_LOG_WARNING,
"getaddrinfo(%s) failed: %s", namebuf,
gai_strerror(result));
gai_strerror(eai));
add(namebuf, ERR_LOOKUP_FAILURE);
}
return true;
@@ -451,19 +534,19 @@ checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
static bool
checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
struct addrinfo hints, *ai, *cur;
struct addrinfo hints = {
.ai_flags = AI_CANONNAME,
.ai_family = PF_UNSPEC,
.ai_socktype = SOCK_STREAM,
.ai_protocol = IPPROTO_TCP,
};
struct addrinfo *ai = NULL, *cur;
char namebuf[DNS_NAME_FORMATSIZE + 1];
char ownerbuf[DNS_NAME_FORMATSIZE];
int result;
int eai;
int level = ISC_LOG_ERROR;
bool answer = true;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
/*
* Turn off search.
@@ -473,9 +556,9 @@ checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
}
dns_name_format(owner, ownerbuf, sizeof(ownerbuf));
result = getaddrinfo(namebuf, NULL, &hints, &ai);
eai = getaddrinfo(namebuf, NULL, &hints, &ai);
dns_name_format(name, namebuf, sizeof(namebuf) - 1);
switch (result) {
switch (eai) {
case 0:
/*
* Work around broken getaddrinfo() implementations that
@@ -508,7 +591,9 @@ checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
}
}
}
freeaddrinfo(ai);
if (ai != NULL) {
freeaddrinfo(ai);
}
return answer;
case EAI_NONAME:
@@ -529,7 +614,7 @@ checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
if (!logged(namebuf, ERR_LOOKUP_FAILURE)) {
dns_zone_log(zone, ISC_LOG_WARNING,
"getaddrinfo(%s) failed: %s", namebuf,
gai_strerror(result));
gai_strerror(eai));
add(namebuf, ERR_LOOKUP_FAILURE);
}
return true;
@@ -603,6 +688,7 @@ load_zone(isc_mem_t *mctx, const char *zonename, const char *filename,
}
if (docheckns) {
dns_zone_setcheckns(zone, checkns);
dns_zone_setcheckisservedby(zone, checkisservedby);
}
if (dochecksrv) {
dns_zone_setchecksrv(zone, checksrv);

View File

@@ -91,9 +91,13 @@ Options
(both in-zone and out-of-zone hostnames). Mode ``local`` only
checks SRV records which refer to in-zone hostnames.
Mode ``full`` checks that a zone that has A or AAAA records it is served
by a server with the same type of address records.
Mode ``full`` checks that delegation NS records refer to A or AAAA
records (both in-zone and out-of-zone hostnames). It also checks that
glue address records in the zone match those advertised by the child.
Mode ``local`` only checks NS records which refer to in-zone
hostnames or verifies that some required glue exists, i.e., when the
name server is in a child zone.

View File

@@ -448,6 +448,9 @@ typedef bool (*dns_checknsfunc_t)(dns_zone_t *, const dns_name_t *,
const dns_name_t *, dns_rdataset_t *,
dns_rdataset_t *);
typedef bool (*dns_checkisservedbyfunc_t)(dns_zone_t *, dns_rdatatype_t type,
const dns_name_t *);
typedef bool (*dns_isselffunc_t)(dns_view_t *, dns_tsigkey_t *,
const isc_sockaddr_t *, const isc_sockaddr_t *,
dns_rdataclass_t, void *);

View File

@@ -2187,6 +2187,18 @@ dns_zone_setcheckns(dns_zone_t *zone, dns_checknsfunc_t checkns);
* 'zone' to be a valid zone.
*/
void
dns_zone_setcheckisservedby(dns_zone_t *zone,
dns_checkisservedbyfunc_t checkisserverby);
/*%<
* Set the post load integrity callback function 'checkisserverby'.
* 'checkisserverby' will be called if the NS TARGET is not within
* the zone and there are A or AAAA records in the the zone.
*
* Require:
* 'zone' to be a valid zone.
*/
void
dns_zone_setnotifydelay(dns_zone_t *zone, uint32_t delay);
/*%<

View File

@@ -377,6 +377,7 @@ struct dns_zone {
dns_checkmxfunc_t checkmx;
dns_checksrvfunc_t checksrv;
dns_checknsfunc_t checkns;
dns_checkisservedbyfunc_t checkisservedby;
/*%
* Zones in certain states such as "waiting for zone transfer"
* or "zone transfer in progress" are kept on per-state linked lists
@@ -2875,8 +2876,8 @@ zone_check_srv(dns_zone_t *zone, dns_db_t *db, dns_name_t *name,
}
static bool
zone_check_glue(dns_zone_t *zone, dns_db_t *db, dns_name_t *name,
dns_name_t *owner) {
zone_check_glue(dns_zone_t *zone, dns_db_t *db, bool *has_a, bool *has_aaaa,
dns_name_t *name, dns_name_t *owner) {
bool answer = true;
isc_result_t result, tresult;
char ownerbuf[DNS_NAME_FORMATSIZE];
@@ -2928,8 +2929,22 @@ zone_check_glue(dns_zone_t *zone, dns_db_t *db, dns_name_t *name,
NULL);
}
if (result == ISC_R_SUCCESS) {
SET_IF_NOT_NULL(has_a, true);
dns_rdataset_disassociate(&a);
if (has_aaaa != NULL && !*has_aaaa) {
result = dns_db_find(db, name, NULL, dns_rdatatype_aaaa,
DNS_DBFIND_GLUEOK, 0, NULL,
foundname, &aaaa, NULL);
if (result == ISC_R_SUCCESS) {
*has_aaaa = true;
}
if (dns_rdataset_isassociated(&aaaa)) {
dns_rdataset_disassociate(&aaaa);
}
}
return true;
} else if (result == DNS_R_GLUE && has_a != NULL) {
*has_a = true;
} else if (result == DNS_R_DELEGATION) {
dns_rdataset_disassociate(&a);
}
@@ -2944,12 +2959,16 @@ zone_check_glue(dns_zone_t *zone, dns_db_t *db, dns_name_t *name,
if (dns_rdataset_isassociated(&a)) {
dns_rdataset_disassociate(&a);
}
SET_IF_NOT_NULL(has_aaaa, true);
dns_rdataset_disassociate(&aaaa);
return true;
}
if (tresult == DNS_R_DELEGATION || tresult == DNS_R_DNAME) {
dns_rdataset_disassociate(&aaaa);
}
if (tresult == DNS_R_GLUE && has_aaaa != NULL) {
*has_aaaa = true;
}
if (result == DNS_R_GLUE || tresult == DNS_R_GLUE) {
/*
* Check glue against child zone.
@@ -3173,6 +3192,46 @@ isspf(const dns_rdata_t *rdata) {
return false;
}
static bool
zone_is_served_by(dns_zone_t *zone, dns_db_t *db, dns_rdatatype_t type,
dns_name_t *name) {
dns_rdataset_t rdataset;
dns_fixedname_t found;
dns_name_t *foundname = dns_fixedname_initname(&found);
isc_result_t result;
/*
* Outside of zone, assume good when loading in named.
*/
if (!dns_name_issubdomain(name, &zone->origin)) {
if (zone->checkisservedby != NULL) {
return zone->checkisservedby(zone, type, name);
}
return true;
}
dns_rdataset_init(&rdataset);
result = dns_db_find(db, name, NULL, type, 0, 0, NULL, foundname,
&rdataset, NULL);
if (dns_rdataset_isassociated(&rdataset)) {
dns_rdataset_disassociate(&rdataset);
}
switch (result) {
case DNS_R_DELEGATION:
if (zone->checkisservedby != NULL) {
return zone->checkisservedby(zone, type, name);
}
/*
* Treat as success.
*/
return true;
case ISC_R_SUCCESS:
return true;
default:
return false;
}
}
static bool
integrity_checks(dns_zone_t *zone, dns_db_t *db) {
dns_dbiterator_t *dbiterator = NULL;
@@ -3188,6 +3247,8 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
dns_name_t *bottom;
isc_result_t result;
bool ok = true, have_spf, have_txt;
bool has_a = false;
bool has_aaaa = false;
int level;
char namebuf[DNS_NAME_FORMATSIZE];
@@ -3242,7 +3303,9 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
dns_rdataset_current(&rdataset, &rdata);
result = dns_rdata_tostruct(&rdata, &ns, NULL);
RUNTIME_CHECK(result == ISC_R_SUCCESS);
if (!zone_check_glue(zone, db, &ns.name, name)) {
if (!zone_check_glue(zone, db, &has_a, &has_aaaa,
&ns.name, name))
{
ok = false;
}
dns_rdata_reset(&rdata);
@@ -3306,7 +3369,7 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_srv,
0, 0, &rdataset, NULL);
if (result != ISC_R_SUCCESS) {
goto checkspf;
goto checkforaaaa;
}
result = dns_rdataset_first(&rdataset);
while (result == ISC_R_SUCCESS) {
@@ -3321,7 +3384,29 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
}
dns_rdataset_disassociate(&rdataset);
checkspf:
checkforaaaa:
/*
* Check if there is an A or AAAA RRset in the zone.
*/
if (!has_a) {
result = dns_db_findrdataset(db, node, NULL,
dns_rdatatype_a, 0, 0,
&rdataset, NULL);
if (result == ISC_R_SUCCESS) {
has_a = true;
dns_rdataset_disassociate(&rdataset);
}
}
if (!has_aaaa) {
result = dns_db_findrdataset(db, node, NULL,
dns_rdatatype_aaaa, 0, 0,
&rdataset, NULL);
if (result == ISC_R_SUCCESS) {
has_aaaa = true;
dns_rdataset_disassociate(&rdataset);
}
}
/*
* Check if there is a type SPF record without an
* SPF-formatted type TXT record also being present.
@@ -3371,6 +3456,70 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
result = dns_dbiterator_next(dbiterator);
}
if (has_a) {
has_a = false;
result = dns_db_find(db, &zone->origin, NULL, dns_rdatatype_ns,
0, 0, NULL, name, &rdataset, NULL);
if (result != ISC_R_SUCCESS) {
if (dns_rdataset_isassociated(&rdataset)) {
dns_rdataset_disassociate(&rdataset);
}
goto cleanup;
}
result = dns_rdataset_first(&rdataset);
while (result == ISC_R_SUCCESS) {
dns_rdataset_current(&rdataset, &rdata);
result = dns_rdata_tostruct(&rdata, &ns, NULL);
RUNTIME_CHECK(result == ISC_R_SUCCESS);
dns_rdata_reset(&rdata);
if (zone_is_served_by(zone, db, dns_rdatatype_a,
&ns.name))
{
has_a = true;
break;
}
result = dns_rdataset_next(&rdataset);
}
dns_rdataset_disassociate(&rdataset);
if (!has_a) {
dns_zone_log(zone, ISC_LOG_WARNING,
"zone has A records but is not served "
"by IPv4 servers");
}
}
if (has_aaaa) {
has_aaaa = false;
result = dns_db_find(db, &zone->origin, NULL, dns_rdatatype_ns,
0, 0, NULL, name, &rdataset, NULL);
if (result != ISC_R_SUCCESS) {
if (dns_rdataset_isassociated(&rdataset)) {
dns_rdataset_disassociate(&rdataset);
}
goto cleanup;
}
result = dns_rdataset_first(&rdataset);
while (result == ISC_R_SUCCESS) {
dns_rdataset_current(&rdataset, &rdata);
result = dns_rdata_tostruct(&rdata, &ns, NULL);
RUNTIME_CHECK(result == ISC_R_SUCCESS);
dns_rdata_reset(&rdata);
if (zone_is_served_by(zone, db, dns_rdatatype_aaaa,
&ns.name))
{
has_aaaa = true;
break;
}
result = dns_rdataset_next(&rdataset);
}
dns_rdataset_disassociate(&rdataset);
if (!has_aaaa) {
dns_zone_log(zone, ISC_LOG_WARNING,
"zone has AAAA records but is not served "
"by IPv6 servers");
}
}
cleanup:
if (node != NULL) {
dns_db_detachnode(db, &node);
@@ -20092,6 +20241,13 @@ dns_zone_setcheckns(dns_zone_t *zone, dns_checknsfunc_t checkns) {
zone->checkns = checkns;
}
void
dns_zone_setcheckisservedby(dns_zone_t *zone,
dns_checkisservedbyfunc_t checkisservedby) {
REQUIRE(DNS_ZONE_VALID(zone));
zone->checkisservedby = checkisservedby;
}
void
dns_zone_setisself(dns_zone_t *zone, dns_isselffunc_t isself, void *arg) {
REQUIRE(DNS_ZONE_VALID(zone));