diff options
Diffstat (limited to 'src/lib/dns/tests/zone_checker_unittest.cc')
-rw-r--r-- | src/lib/dns/tests/zone_checker_unittest.cc | 349 |
1 files changed, 349 insertions, 0 deletions
diff --git a/src/lib/dns/tests/zone_checker_unittest.cc b/src/lib/dns/tests/zone_checker_unittest.cc new file mode 100644 index 0000000..863b439 --- /dev/null +++ b/src/lib/dns/tests/zone_checker_unittest.cc @@ -0,0 +1,349 @@ +// Copyright (C) 2012-2020 Internet Systems Consortium, Inc. ("ISC") +// +// 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 http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <dns/zone_checker.h> + +#include <exceptions/exceptions.h> + +#include <dns/name.h> +#include <dns/rrclass.h> +#include <dns/rrset.h> +#include <dns/rrtype.h> +#include <dns/rrttl.h> +#include <dns/rdataclass.h> +#include <dns/rrset_collection.h> + +#include <gtest/gtest.h> + +#include <boost/scoped_ptr.hpp> + +#include <algorithm> +#include <functional> +#include <string> +#include <sstream> +#include <vector> + +using isc::Unexpected; +using namespace isc::dns; +using namespace isc::dns::rdata; +namespace ph = std::placeholders; + +namespace { + +const char* const soa_txt = "ns.example.com. root.example.com. 0 0 0 0 0"; +const char* const ns_txt1 = "ns.example.com."; +const char* const ns_a_txt1 = "192.0.2.1"; +const char* const ns_txt2 = "ns2.example.com."; +const char* const ns_a_txt2 = "192.0.2.2"; + +class ZoneCheckerTest : public ::testing::Test { +protected: + ZoneCheckerTest() : + zname_("example.com"), zclass_(RRClass::IN()), + soa_(new RRset(zname_, zclass_, RRType::SOA(), RRTTL(60))), + ns_(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60))), + callbacks_(std::bind(&ZoneCheckerTest::callback, this, ph::_1, true), + std::bind(&ZoneCheckerTest::callback, this, ph::_1, false)) + { + std::stringstream ss; + ss << "example.com. 60 IN SOA " << soa_txt << "\n"; + ss << "example.com. 60 IN NS " << ns_txt1 << "\n"; + ss << "ns.example.com. 60 IN A " << ns_a_txt1 << "\n"; + ss << "ns2.example.com. 60 IN A " << ns_a_txt2 << "\n"; + rrsets_.reset(new RRsetCollection(ss, zname_, zclass_)); + } + +public: + // This one is passed to std::bind. Some compilers seem to require + // it be public. + void callback(const std::string& reason, bool is_error) { + if (is_error) { + errors_.push_back(reason); + } else { + warns_.push_back(reason); + } + } + +protected: + // Check stored issue messages with expected ones. Clear vectors so + // the caller can check other cases. + void checkIssues() { + EXPECT_EQ(expected_errors_.size(), errors_.size()); + for (size_t i = 0; + i < std::min(expected_errors_.size(), errors_.size()); + ++i) { + // The actual message should begin with the expected message. + EXPECT_EQ(0, errors_[0].find(expected_errors_[0])) + << "actual message: " << errors_[0] << " expected: " << + expected_errors_[0]; + } + EXPECT_EQ(expected_warns_.size(), warns_.size()); + for (size_t i = 0; + i < std::min(expected_warns_.size(), warns_.size()); + ++i) { + EXPECT_EQ(0, warns_[0].find(expected_warns_[0])) + << "actual message: " << warns_[0] << " expected: " << + expected_warns_[0]; + } + + errors_.clear(); + expected_errors_.clear(); + warns_.clear(); + expected_warns_.clear(); + } + + const Name zname_; + const RRClass zclass_; + boost::scoped_ptr<RRsetCollection> rrsets_; + RRsetPtr soa_; + RRsetPtr ns_; + std::vector<std::string> errors_; + std::vector<std::string> warns_; + std::vector<std::string> expected_errors_; + std::vector<std::string> expected_warns_; + ZoneCheckerCallbacks callbacks_; +}; + +TEST_F(ZoneCheckerTest, checkGood) { + // Checking a valid case. No errors or warnings should be reported. + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + checkIssues(); + + // Multiple NS RRs are okay. + rrsets_->removeRRset(zname_, zclass_, RRType::NS()); + ns_->addRdata(generic::NS(ns_txt1)); + ns_->addRdata(generic::NS(ns_txt2)); + rrsets_->addRRset(ns_); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + checkIssues(); +} + +TEST_F(ZoneCheckerTest, checkSOA) { + // If the zone has no SOA it triggers an error. + rrsets_->removeRRset(zname_, zclass_, RRType::SOA()); + EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_errors_.push_back("zone example.com/IN: has 0 SOA records"); + checkIssues(); + + // If null callback is specified, checkZone() only returns the final + // result. + ZoneCheckerCallbacks noerror_callbacks( + 0, std::bind(&ZoneCheckerTest::callback, this, ph::_1, false)); + EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, noerror_callbacks)); + checkIssues(); + + // If there are more than 1 SOA RR, it's also an error. + errors_.clear(); + soa_->addRdata(generic::SOA(soa_txt)); + soa_->addRdata(generic::SOA("ns2.example.com. . 0 0 0 0 0")); + rrsets_->addRRset(soa_); + EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_errors_.push_back("zone example.com/IN: has 2 SOA records"); + checkIssues(); + + // If the SOA RRset is "empty", it's treated as an implementation + // (rather than operational) error and results in an exception. + rrsets_->removeRRset(zname_, zclass_, RRType::SOA()); + soa_.reset(new RRset(zname_, zclass_, RRType::SOA(), RRTTL(60))); + rrsets_->addRRset(soa_); + EXPECT_THROW(checkZone(zname_, zclass_, *rrsets_, callbacks_), Unexpected); + checkIssues(); // no error/warning should be reported + + // Likewise, if the SOA RRset contains non SOA Rdata, it should be a bug. + rrsets_->removeRRset(zname_, zclass_, RRType::SOA()); + soa_.reset(new RRset(zname_, zclass_, RRType::SOA(), RRTTL(60))); + soa_->addRdata(createRdata(RRType::NS(), zclass_, "ns.example.com.")); + rrsets_->addRRset(soa_); + EXPECT_THROW(checkZone(zname_, zclass_, *rrsets_, callbacks_), Unexpected); + checkIssues(); // no error/warning should be reported +} + +TEST_F(ZoneCheckerTest, checkNS) { + // If the zone has no NS at origin it triggers an error. + rrsets_->removeRRset(zname_, zclass_, RRType::NS()); + EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_errors_.push_back("zone example.com/IN: has no NS records"); + checkIssues(); + + // Check two buggy cases like the SOA tests + ns_.reset(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60))); + rrsets_->addRRset(ns_); + EXPECT_THROW(checkZone(zname_, zclass_, *rrsets_, callbacks_), Unexpected); + checkIssues(); // no error/warning should be reported + + rrsets_->removeRRset(zname_, zclass_, RRType::NS()); + ns_.reset(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60))); + ns_->addRdata(createRdata(RRType::TXT(), zclass_, "ns.example.com")); + rrsets_->addRRset(ns_); + EXPECT_THROW(checkZone(zname_, zclass_, *rrsets_, callbacks_), Unexpected); + checkIssues(); // no error/warning should be reported +} + +TEST_F(ZoneCheckerTest, checkNSData) { + const Name ns_name("ns.example.com"); + + // If a ("in-bailiwick") NS name doesn't have an address record, it's + // reported as a warning. + rrsets_->removeRRset(ns_name, zclass_, RRType::A()); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_warns_.push_back("zone example.com/IN: NS has no address"); + checkIssues(); + + // Same check, but disabling warning callback. Same result, but without + // the warning. + ZoneCheckerCallbacks nowarn_callbacks( + std::bind(&ZoneCheckerTest::callback, this, ph::_1, true), 0); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, nowarn_callbacks)); + checkIssues(); + + // A tricky case: if the name matches a wildcard, it should technically + // be considered valid, but this checker doesn't check that far and still + // warns. + RRsetPtr wild(new RRset(Name("*.example.com"), zclass_, RRType::A(), + RRTTL(0))); + wild->addRdata(in::A("192.0.2.255")); + rrsets_->addRRset(wild); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_warns_.push_back("zone example.com/IN: NS has no address"); + checkIssues(); + + // If there's a CNAME at the name instead, it's an error. + rrsets_->removeRRset(Name("*.example.com"), zclass_, RRType::A()); + RRsetPtr cname(new RRset(ns_name, zclass_, RRType::CNAME(), RRTTL(60))); + cname->addRdata(generic::CNAME("cname.example.com.")); + rrsets_->addRRset(cname); + EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_errors_.push_back("zone example.com/IN: NS 'ns.example.com' is " + "a CNAME (illegal per RFC2181)"); + checkIssues(); + + // It doesn't have to be A. An AAAA is enough. + rrsets_->removeRRset(ns_name, zclass_, RRType::CNAME()); + RRsetPtr aaaa(new RRset(ns_name, zclass_, RRType::AAAA(), RRTTL(60))); + aaaa->addRdata(in::AAAA("2001:db8::1")); + rrsets_->addRRset(aaaa); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + checkIssues(); + + // Coexisting CNAME makes it error (CNAME with other record is itself + // invalid, but it's a different issue in this context) + rrsets_->addRRset(cname); + EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_errors_.push_back("zone example.com/IN: NS 'ns.example.com' is " + "a CNAME (illegal per RFC2181)"); + checkIssues(); + + // It doesn't matter if the NS name is "out of bailiwick". + rrsets_->removeRRset(ns_name, zclass_, RRType::CNAME()); + rrsets_->removeRRset(zname_, zclass_, RRType::NS()); + ns_.reset(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60))); + ns_->addRdata(generic::NS("ns.example.org.")); + rrsets_->addRRset(ns_); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + checkIssues(); + + // Note that if the NS name is the origin name, it should be checked + rrsets_->removeRRset(zname_, zclass_, RRType::NS()); + ns_.reset(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60))); + ns_->addRdata(generic::NS(zname_)); + rrsets_->addRRset(ns_); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_warns_.push_back("zone example.com/IN: NS has no address"); + checkIssues(); +} + +TEST_F(ZoneCheckerTest, checkNSWithDelegation) { + // Tests various cases where there's a zone cut due to delegation between + // the zone origin and the NS name. In each case the NS name doesn't have + // an address record. + const Name ns_name("ns.child.example.com"); + + // Zone cut due to delegation in the middle; the check for the address + // record should be skipped. + rrsets_->removeRRset(zname_, zclass_, RRType::NS()); + ns_.reset(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60))); + ns_->addRdata(generic::NS(ns_name)); + rrsets_->addRRset(ns_); + RRsetPtr child_ns(new RRset(Name("child.example.com"), zclass_, + RRType::NS(), RRTTL(60))); + child_ns->addRdata(generic::NS("ns.example.org.")); + rrsets_->addRRset(child_ns); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + checkIssues(); + + // Zone cut at the NS name. Same result. + rrsets_->removeRRset(child_ns->getName(), zclass_, RRType::NS()); + child_ns.reset(new RRset(ns_name, zclass_, RRType::NS(), RRTTL(60))); + child_ns->addRdata(generic::NS("ns.example.org.")); + rrsets_->addRRset(child_ns); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + checkIssues(); + + // Zone cut below the NS name. The check applies. + rrsets_->removeRRset(child_ns->getName(), zclass_, RRType::NS()); + child_ns.reset(new RRset(Name("another.ns.child.example.com"), zclass_, + RRType::NS(), RRTTL(60))); + child_ns->addRdata(generic::NS("ns.example.org.")); + rrsets_->addRRset(child_ns); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_warns_.push_back("zone example.com/IN: NS has no address"); + checkIssues(); +} + +TEST_F(ZoneCheckerTest, checkNSWithDNAME) { + // Similar to the above case, but the zone cut is due to DNAME. This is + // an invalid configuration. + const Name ns_name("ns.child.example.com"); + + // Zone cut due to DNAME at the zone origin. This is an invalid case. + rrsets_->removeRRset(zname_, zclass_, RRType::NS()); + ns_.reset(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60))); + ns_->addRdata(generic::NS(ns_name)); + rrsets_->addRRset(ns_); + RRsetPtr dname(new RRset(zname_, zclass_, RRType::DNAME(), RRTTL(60))); + dname->addRdata(generic::DNAME("example.org.")); + rrsets_->addRRset(dname); + EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_errors_.push_back("zone example.com/IN: NS 'ns.child.example.com'" + " is below a DNAME 'example.com'"); + checkIssues(); + + // Zone cut due to DNAME in the middle. Same result. + rrsets_->removeRRset(zname_, zclass_, RRType::DNAME()); + dname.reset(new RRset(Name("child.example.com"), zclass_, RRType::DNAME(), + RRTTL(60))); + dname->addRdata(generic::DNAME("example.org.")); + rrsets_->addRRset(dname); + EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_errors_.push_back("zone example.com/IN: NS 'ns.child.example.com'" + " is below a DNAME 'child.example.com'"); + checkIssues(); + + // A tricky case: there's also an NS at the name that has DNAME. It's + // prohibited per RFC6672 so we could say it's "undefined". Nevertheless, + // this implementation prefers the NS and skips further checks. + ns_.reset(new RRset(Name("child.example.com"), zclass_, RRType::NS(), + RRTTL(60))); + ns_->addRdata(generic::NS("ns.example.org.")); + rrsets_->addRRset(ns_); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + checkIssues(); + + // Zone cut due to DNAME at the NS name. In this case DNAME doesn't + // affect the NS name, so it should result in "no address record" warning. + rrsets_->removeRRset(dname->getName(), zclass_, RRType::DNAME()); + rrsets_->removeRRset(ns_->getName(), zclass_, RRType::NS()); + dname.reset(new RRset(ns_name, zclass_, RRType::DNAME(), RRTTL(60))); + dname->addRdata(generic::DNAME("example.org.")); + rrsets_->addRRset(dname); + EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_)); + expected_warns_.push_back("zone example.com/IN: NS has no address"); + checkIssues(); +} + +} |