/* $NetBSD: check-tool.c,v 1.8.2.2 2024/02/25 15:42:59 martin Exp $ */ /* * 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. */ /*! \file */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "check-tool.h" #ifndef CHECK_SIBLING #define CHECK_SIBLING 1 #endif /* ifndef CHECK_SIBLING */ #ifndef CHECK_LOCAL #define CHECK_LOCAL 1 #endif /* ifndef CHECK_LOCAL */ #define CHECK(r) \ do { \ result = (r); \ if (result != ISC_R_SUCCESS) \ goto cleanup; \ } while (0) #define ERR_IS_CNAME 1 #define ERR_NO_ADDRESSES 2 #define ERR_LOOKUP_FAILURE 3 #define ERR_EXTRA_A 4 #define ERR_EXTRA_AAAA 5 #define ERR_MISSING_GLUE 5 #define ERR_IS_MXCNAME 6 #define ERR_IS_SRVCNAME 7 static const char *dbtype[] = { "rbt" }; int debug = 0; const char *journal = NULL; bool nomerge = true; #if CHECK_LOCAL bool docheckmx = true; bool dochecksrv = true; bool docheckns = true; #else /* if CHECK_LOCAL */ bool docheckmx = false; bool dochecksrv = false; bool docheckns = false; #endif /* if CHECK_LOCAL */ dns_zoneopt_t zone_options = DNS_ZONEOPT_CHECKNS | DNS_ZONEOPT_CHECKMX | DNS_ZONEOPT_MANYERRORS | DNS_ZONEOPT_CHECKNAMES | DNS_ZONEOPT_CHECKINTEGRITY | #if CHECK_SIBLING DNS_ZONEOPT_CHECKSIBLING | #endif /* if CHECK_SIBLING */ DNS_ZONEOPT_CHECKWILDCARD | DNS_ZONEOPT_WARNMXCNAME | DNS_ZONEOPT_WARNSRVCNAME; /* * This needs to match the list in bin/named/log.c. */ static isc_logcategory_t categories[] = { { "", 0 }, { "unmatched", 0 }, { NULL, 0 } }; static isc_symtab_t *symtab = NULL; static isc_mem_t *sym_mctx; static void freekey(char *key, unsigned int type, isc_symvalue_t value, void *userarg) { UNUSED(type); UNUSED(value); isc_mem_free(userarg, key); } static void add(char *key, int value) { isc_result_t result; isc_symvalue_t symvalue; if (sym_mctx == NULL) { isc_mem_create(&sym_mctx); } if (symtab == NULL) { result = isc_symtab_create(sym_mctx, 100, freekey, sym_mctx, false, &symtab); if (result != ISC_R_SUCCESS) { return; } } key = isc_mem_strdup(sym_mctx, key); symvalue.as_pointer = NULL; result = isc_symtab_define(symtab, key, value, symvalue, isc_symexists_reject); if (result != ISC_R_SUCCESS) { isc_mem_free(sym_mctx, key); } } static bool logged(char *key, int value) { isc_result_t result; if (symtab == NULL) { return (false); } result = isc_symtab_lookup(symtab, key, value, NULL); if (result == ISC_R_SUCCESS) { return (true); } return (false); } 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; char namebuf[DNS_NAME_FORMATSIZE + 1]; char ownerbuf[DNS_NAME_FORMATSIZE]; char addrbuf[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:123.123.123.123")]; bool answer = true; bool match; const char *type; void *ptr = NULL; int result; REQUIRE(a == NULL || !dns_rdataset_isassociated(a) || a->type == dns_rdatatype_a); REQUIRE(aaaa == NULL || !dns_rdataset_isassociated(aaaa) || aaaa->type == dns_rdatatype_aaaa); if (a == NULL || aaaa == NULL) { 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. */ if (dns_name_countlabels(name) > 1U) { strlcat(namebuf, ".", sizeof(namebuf)); } dns_name_format(owner, ownerbuf, sizeof(ownerbuf)); result = getaddrinfo(namebuf, NULL, &hints, &ai); dns_name_format(name, namebuf, sizeof(namebuf) - 1); switch (result) { case 0: /* * Work around broken getaddrinfo() implementations that * fail to set ai_canonname on first entry. */ cur = ai; while (cur != NULL && cur->ai_canonname == NULL && cur->ai_next != NULL) { cur = cur->ai_next; } if (cur != NULL && cur->ai_canonname != NULL && strcasecmp(cur->ai_canonname, namebuf) != 0 && !logged(namebuf, ERR_IS_CNAME)) { dns_zone_log(zone, ISC_LOG_ERROR, "%s/NS '%s' (out of zone) " "is a CNAME '%s' (illegal)", ownerbuf, namebuf, cur->ai_canonname); /* XXX950 make fatal for 9.5.0 */ /* answer = false; */ add(namebuf, ERR_IS_CNAME); } break; case EAI_NONAME: #if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME) case EAI_NODATA: #endif /* if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME) */ if (!logged(namebuf, ERR_NO_ADDRESSES)) { 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); } /* XXX950 make fatal for 9.5.0 */ return (true); default: if (!logged(namebuf, ERR_LOOKUP_FAILURE)) { dns_zone_log(zone, ISC_LOG_WARNING, "getaddrinfo(%s) failed: %s", namebuf, gai_strerror(result)); add(namebuf, ERR_LOOKUP_FAILURE); } return (true); } /* * Check that all glue records really exist. */ if (!dns_rdataset_isassociated(a)) { goto checkaaaa; } result = dns_rdataset_first(a); while (result == ISC_R_SUCCESS) { dns_rdataset_current(a, &rdata); match = false; for (cur = ai; cur != NULL; cur = cur->ai_next) { if (cur->ai_family != AF_INET) { continue; } ptr = &((struct sockaddr_in *)(cur->ai_addr))->sin_addr; if (memcmp(ptr, rdata.data, rdata.length) == 0) { match = true; break; } } if (!match && !logged(namebuf, ERR_EXTRA_A)) { dns_zone_log(zone, ISC_LOG_ERROR, "%s/NS '%s' " "extra GLUE A record (%s)", ownerbuf, namebuf, inet_ntop(AF_INET, rdata.data, addrbuf, sizeof(addrbuf))); add(namebuf, ERR_EXTRA_A); /* XXX950 make fatal for 9.5.0 */ /* answer = false; */ } dns_rdata_reset(&rdata); result = dns_rdataset_next(a); } checkaaaa: if (!dns_rdataset_isassociated(aaaa)) { goto checkmissing; } result = dns_rdataset_first(aaaa); while (result == ISC_R_SUCCESS) { dns_rdataset_current(aaaa, &rdata); match = false; for (cur = ai; cur != NULL; cur = cur->ai_next) { if (cur->ai_family != AF_INET6) { continue; } ptr = &((struct sockaddr_in6 *)(cur->ai_addr)) ->sin6_addr; if (memcmp(ptr, rdata.data, rdata.length) == 0) { match = true; break; } } if (!match && !logged(namebuf, ERR_EXTRA_AAAA)) { dns_zone_log(zone, ISC_LOG_ERROR, "%s/NS '%s' " "extra GLUE AAAA record (%s)", ownerbuf, namebuf, inet_ntop(AF_INET6, rdata.data, addrbuf, sizeof(addrbuf))); add(namebuf, ERR_EXTRA_AAAA); /* XXX950 make fatal for 9.5.0. */ /* answer = false; */ } dns_rdata_reset(&rdata); result = dns_rdataset_next(aaaa); } checkmissing: /* * Check that all addresses appear in the glue. */ if (!logged(namebuf, ERR_MISSING_GLUE)) { bool missing_glue = false; for (cur = ai; cur != NULL; cur = cur->ai_next) { switch (cur->ai_family) { case AF_INET: rdataset = a; ptr = &((struct sockaddr_in *)(cur->ai_addr)) ->sin_addr; type = "A"; break; case AF_INET6: rdataset = aaaa; ptr = &((struct sockaddr_in6 *)(cur->ai_addr)) ->sin6_addr; type = "AAAA"; break; default: continue; } match = false; if (dns_rdataset_isassociated(rdataset)) { result = dns_rdataset_first(rdataset); } else { result = ISC_R_FAILURE; } while (result == ISC_R_SUCCESS && !match) { dns_rdataset_current(rdataset, &rdata); if (memcmp(ptr, rdata.data, rdata.length) == 0) { match = true; } dns_rdata_reset(&rdata); result = dns_rdataset_next(rdataset); } if (!match) { dns_zone_log(zone, ISC_LOG_ERROR, "%s/NS '%s' " "missing GLUE %s record (%s)", ownerbuf, namebuf, type, inet_ntop(cur->ai_family, ptr, addrbuf, sizeof(addrbuf))); /* XXX950 make fatal for 9.5.0. */ /* answer = false; */ missing_glue = true; } } if (missing_glue) { add(namebuf, ERR_MISSING_GLUE); } } 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; char namebuf[DNS_NAME_FORMATSIZE + 1]; char ownerbuf[DNS_NAME_FORMATSIZE]; int result; 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. */ if (dns_name_countlabels(name) > 1U) { strlcat(namebuf, ".", sizeof(namebuf)); } dns_name_format(owner, ownerbuf, sizeof(ownerbuf)); result = getaddrinfo(namebuf, NULL, &hints, &ai); dns_name_format(name, namebuf, sizeof(namebuf) - 1); switch (result) { case 0: /* * Work around broken getaddrinfo() implementations that * fail to set ai_canonname on first entry. */ cur = ai; while (cur != NULL && cur->ai_canonname == NULL && cur->ai_next != NULL) { cur = cur->ai_next; } if (cur != NULL && cur->ai_canonname != NULL && strcasecmp(cur->ai_canonname, namebuf) != 0) { if ((zone_options & DNS_ZONEOPT_WARNMXCNAME) != 0) { level = ISC_LOG_WARNING; } if ((zone_options & DNS_ZONEOPT_IGNOREMXCNAME) == 0) { if (!logged(namebuf, ERR_IS_MXCNAME)) { dns_zone_log(zone, level, "%s/MX '%s' (out of zone)" " is a CNAME '%s' " "(illegal)", ownerbuf, namebuf, cur->ai_canonname); add(namebuf, ERR_IS_MXCNAME); } if (level == ISC_LOG_ERROR) { answer = false; } } } freeaddrinfo(ai); return (answer); case EAI_NONAME: #if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME) case EAI_NODATA: #endif /* if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME) */ if (!logged(namebuf, ERR_NO_ADDRESSES)) { dns_zone_log(zone, ISC_LOG_ERROR, "%s/MX '%s' (out of zone) " "has no addresses records (A or AAAA)", ownerbuf, namebuf); add(namebuf, ERR_NO_ADDRESSES); } /* XXX950 make fatal for 9.5.0. */ return (true); default: if (!logged(namebuf, ERR_LOOKUP_FAILURE)) { dns_zone_log(zone, ISC_LOG_WARNING, "getaddrinfo(%s) failed: %s", namebuf, gai_strerror(result)); add(namebuf, ERR_LOOKUP_FAILURE); } return (true); } } static bool checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) { struct addrinfo hints, *ai, *cur; char namebuf[DNS_NAME_FORMATSIZE + 1]; char ownerbuf[DNS_NAME_FORMATSIZE]; int result; 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. */ if (dns_name_countlabels(name) > 1U) { strlcat(namebuf, ".", sizeof(namebuf)); } dns_name_format(owner, ownerbuf, sizeof(ownerbuf)); result = getaddrinfo(namebuf, NULL, &hints, &ai); dns_name_format(name, namebuf, sizeof(namebuf) - 1); switch (result) { case 0: /* * Work around broken getaddrinfo() implementations that * fail to set ai_canonname on first entry. */ cur = ai; while (cur != NULL && cur->ai_canonname == NULL && cur->ai_next != NULL) { cur = cur->ai_next; } if (cur != NULL && cur->ai_canonname != NULL && strcasecmp(cur->ai_canonname, namebuf) != 0) { if ((zone_options & DNS_ZONEOPT_WARNSRVCNAME) != 0) { level = ISC_LOG_WARNING; } if ((zone_options & DNS_ZONEOPT_IGNORESRVCNAME) == 0) { if (!logged(namebuf, ERR_IS_SRVCNAME)) { dns_zone_log(zone, level, "%s/SRV '%s'" " (out of zone) is a " "CNAME '%s' (illegal)", ownerbuf, namebuf, cur->ai_canonname); add(namebuf, ERR_IS_SRVCNAME); } if (level == ISC_LOG_ERROR) { answer = false; } } } freeaddrinfo(ai); return (answer); case EAI_NONAME: #if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME) case EAI_NODATA: #endif /* if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME) */ if (!logged(namebuf, ERR_NO_ADDRESSES)) { dns_zone_log(zone, ISC_LOG_ERROR, "%s/SRV '%s' (out of zone) " "has no addresses records (A or AAAA)", ownerbuf, namebuf); add(namebuf, ERR_NO_ADDRESSES); } /* XXX950 make fatal for 9.5.0. */ return (true); default: if (!logged(namebuf, ERR_LOOKUP_FAILURE)) { dns_zone_log(zone, ISC_LOG_WARNING, "getaddrinfo(%s) failed: %s", namebuf, gai_strerror(result)); add(namebuf, ERR_LOOKUP_FAILURE); } return (true); } } isc_result_t setup_logging(isc_mem_t *mctx, FILE *errout, isc_log_t **logp) { isc_logdestination_t destination; isc_logconfig_t *logconfig = NULL; isc_log_t *log = NULL; isc_log_create(mctx, &log, &logconfig); isc_log_registercategories(log, categories); isc_log_setcontext(log); dns_log_init(log); dns_log_setcontext(log); cfg_log_init(log); ns_log_init(log); destination.file.stream = errout; destination.file.name = NULL; destination.file.versions = ISC_LOG_ROLLNEVER; destination.file.maximum_size = 0; isc_log_createchannel(logconfig, "stderr", ISC_LOG_TOFILEDESC, ISC_LOG_DYNAMIC, &destination, 0); RUNTIME_CHECK(isc_log_usechannel(logconfig, "stderr", NULL, NULL) == ISC_R_SUCCESS); *logp = log; return (ISC_R_SUCCESS); } /*% load the zone */ isc_result_t load_zone(isc_mem_t *mctx, const char *zonename, const char *filename, dns_masterformat_t fileformat, const char *classname, dns_ttl_t maxttl, dns_zone_t **zonep) { isc_result_t result; dns_rdataclass_t rdclass; isc_textregion_t region; isc_buffer_t buffer; dns_fixedname_t fixorigin; dns_name_t *origin; dns_zone_t *zone = NULL; REQUIRE(zonep == NULL || *zonep == NULL); if (debug) { fprintf(stderr, "loading \"%s\" from \"%s\" class \"%s\"\n", zonename, filename, classname); } CHECK(dns_zone_create(&zone, mctx)); dns_zone_settype(zone, dns_zone_primary); isc_buffer_constinit(&buffer, zonename, strlen(zonename)); isc_buffer_add(&buffer, strlen(zonename)); origin = dns_fixedname_initname(&fixorigin); CHECK(dns_name_fromtext(origin, &buffer, dns_rootname, 0, NULL)); CHECK(dns_zone_setorigin(zone, origin)); dns_zone_setdbtype(zone, 1, (const char *const *)dbtype); if (strcmp(filename, "-") == 0) { CHECK(dns_zone_setstream(zone, stdin, fileformat, &dns_master_style_default)); } else { CHECK(dns_zone_setfile(zone, filename, fileformat, &dns_master_style_default)); } if (journal != NULL) { CHECK(dns_zone_setjournal(zone, journal)); } DE_CONST(classname, region.base); region.length = strlen(classname); CHECK(dns_rdataclass_fromtext(&rdclass, ®ion)); dns_zone_setclass(zone, rdclass); dns_zone_setoption(zone, zone_options, true); dns_zone_setoption(zone, DNS_ZONEOPT_NOMERGE, nomerge); dns_zone_setmaxttl(zone, maxttl); if (docheckmx) { dns_zone_setcheckmx(zone, checkmx); } if (docheckns) { dns_zone_setcheckns(zone, checkns); } if (dochecksrv) { dns_zone_setchecksrv(zone, checksrv); } CHECK(dns_zone_load(zone, false)); if (zonep != NULL) { *zonep = zone; zone = NULL; } cleanup: if (zone != NULL) { dns_zone_detach(&zone); } return (result); } /*% dump the zone */ isc_result_t dump_zone(const char *zonename, dns_zone_t *zone, const char *filename, dns_masterformat_t fileformat, const dns_master_style_t *style, const uint32_t rawversion) { isc_result_t result; FILE *output = stdout; const char *flags; flags = (fileformat == dns_masterformat_text) ? "w" : "wb"; if (debug) { if (filename != NULL && strcmp(filename, "-") != 0) { fprintf(stderr, "dumping \"%s\" to \"%s\"\n", zonename, filename); } else { fprintf(stderr, "dumping \"%s\"\n", zonename); } } if (filename != NULL && strcmp(filename, "-") != 0) { result = isc_stdio_open(filename, flags, &output); if (result != ISC_R_SUCCESS) { fprintf(stderr, "could not open output " "file \"%s\" for writing\n", filename); return (ISC_R_FAILURE); } } result = dns_zone_dumptostream(zone, output, fileformat, style, rawversion); if (output != stdout) { (void)isc_stdio_close(output); } return (result); }