1 // Copyright (C) 2012-2020 Internet Systems Consortium, Inc. ("ISC")
2 //
3 // This Source Code Form is subject to the terms of the Mozilla Public
4 // License, v. 2.0. If a copy of the MPL was not distributed with this
5 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 
7 #include <config.h>
8 
9 #include <dns/zone_checker.h>
10 
11 #include <dns/name.h>
12 #include <dns/rdataclass.h>
13 #include <dns/rrclass.h>
14 #include <dns/rrtype.h>
15 #include <dns/rrset.h>
16 #include <dns/rrset_collection_base.h>
17 
18 #include <boost/lexical_cast.hpp>
19 
20 #include <functional>
21 #include <string>
22 
23 using boost::lexical_cast;
24 using std::string;
25 namespace ph = std::placeholders;
26 
27 namespace isc {
28 namespace dns {
29 
30 namespace {
31 std::string
zoneText(const Name & zone_name,const RRClass & zone_class)32 zoneText(const Name& zone_name, const RRClass& zone_class) {
33     return (zone_name.toText(true) + "/" + zone_class.toText());
34 }
35 
36 void
checkSOA(const Name & zone_name,const RRClass & zone_class,const RRsetCollectionBase & zone_rrsets,ZoneCheckerCallbacks & callback)37 checkSOA(const Name& zone_name, const RRClass& zone_class,
38          const RRsetCollectionBase& zone_rrsets,
39          ZoneCheckerCallbacks& callback) {
40     ConstRRsetPtr rrset =
41         zone_rrsets.find(zone_name, zone_class, RRType::SOA());
42     size_t count = 0;
43     if (rrset) {
44         for (RdataIteratorPtr rit = rrset->getRdataIterator();
45              !rit->isLast();
46              rit->next(), ++count) {
47             if (dynamic_cast<const rdata::generic::SOA*>(&rit->getCurrent()) ==
48                 NULL) {
49                 isc_throw(Unexpected, "Zone checker found bad RDATA in SOA");
50             }
51         }
52         if (count == 0) {
53             // this should be an implementation bug, not an operational error.
54             isc_throw(Unexpected, "Zone checker found an empty SOA RRset");
55         }
56     }
57     if (count != 1) {
58         callback.error("zone " + zoneText(zone_name, zone_class) + ": has " +
59                        lexical_cast<string>(count) + " SOA records");
60     }
61 }
62 
63 // Check if a target name is beyond zone cut, either due to delegation or
64 // DNAME.  Note that DNAME works on the origin but not on the name itself,
65 // while delegation works on the name itself (but the NS at the origin is not
66 // delegation).
67 ConstRRsetPtr
findZoneCut(const Name & zone_name,const RRClass & zone_class,const RRsetCollectionBase & zone_rrsets,const Name & target_name)68 findZoneCut(const Name& zone_name, const RRClass& zone_class,
69             const RRsetCollectionBase& zone_rrsets, const Name& target_name) {
70     const unsigned int origin_count = zone_name.getLabelCount();
71     const unsigned int target_count = target_name.getLabelCount();
72     assert(origin_count <= target_count);
73 
74     for (unsigned int l = origin_count; l <= target_count; ++l) {
75         const Name& mid_name = (l == target_count) ? target_name :
76             target_name.split(target_count - l);
77 
78         ConstRRsetPtr found;
79         if (l != origin_count &&
80             (found = zone_rrsets.find(mid_name, zone_class, RRType::NS())) !=
81             NULL) {
82             return (found);
83         }
84         if (l != target_count &&
85             (found = zone_rrsets.find(mid_name, zone_class, RRType::DNAME()))
86             != NULL) {
87             return (found);
88         }
89     }
90     return (ConstRRsetPtr());
91 }
92 
93 // Check if each "in-zone" NS name has an address record, identifying some
94 // error cases.
95 void
checkNSNames(const Name & zone_name,const RRClass & zone_class,const RRsetCollectionBase & zone_rrsets,ConstRRsetPtr ns_rrset,ZoneCheckerCallbacks & callbacks)96 checkNSNames(const Name& zone_name, const RRClass& zone_class,
97              const RRsetCollectionBase& zone_rrsets,
98              ConstRRsetPtr ns_rrset, ZoneCheckerCallbacks& callbacks) {
99     if (ns_rrset->getRdataCount() == 0) {
100         // this should be an implementation bug, not an operational error.
101         isc_throw(Unexpected, "Zone checker found an empty NS RRset");
102     }
103 
104     for (RdataIteratorPtr rit = ns_rrset->getRdataIterator();
105          !rit->isLast();
106          rit->next()) {
107         const rdata::generic::NS* ns_data =
108             dynamic_cast<const rdata::generic::NS*>(&rit->getCurrent());
109         if (ns_data == NULL) {
110             isc_throw(Unexpected, "Zone checker found bad RDATA in NS");
111         }
112         const Name& ns_name = ns_data->getNSName();
113         const NameComparisonResult::NameRelation reln =
114             ns_name.compare(zone_name).getRelation();
115         if (reln != NameComparisonResult::EQUAL &&
116             reln != NameComparisonResult::SUBDOMAIN) {
117             continue;           // not in the zone.  we can ignore it.
118         }
119 
120         // Check if there's a zone cut between the origin and the NS name.
121         ConstRRsetPtr cut_rrset = findZoneCut(zone_name, zone_class,
122                                               zone_rrsets, ns_name);
123         if (cut_rrset) {
124             if  (cut_rrset->getType() == RRType::NS()) {
125                 continue; // delegation; making the NS name "out of zone".
126             } else if (cut_rrset->getType() == RRType::DNAME()) {
127                 callbacks.error("zone " + zoneText(zone_name, zone_class) +
128                                 ": NS '" + ns_name.toText(true) + "' is " +
129                                 "below a DNAME '" +
130                                 cut_rrset->getName().toText(true) +
131                                 "' (illegal per RFC6672)");
132                 continue;
133             } else {
134                 assert(false);
135             }
136         }
137         if (zone_rrsets.find(ns_name, zone_class, RRType::CNAME()) != NULL) {
138             callbacks.error("zone " + zoneText(zone_name, zone_class) +
139                             ": NS '" + ns_name.toText(true) + "' is a CNAME " +
140                             "(illegal per RFC2181)");
141             continue;
142         }
143         if (zone_rrsets.find(ns_name, zone_class, RRType::A()) == NULL &&
144             zone_rrsets.find(ns_name, zone_class, RRType::AAAA()) == NULL) {
145             callbacks.warn("zone " + zoneText(zone_name, zone_class) +
146                            ": NS has no address records (A or AAAA)");
147         }
148     }
149 }
150 
151 void
checkNS(const Name & zone_name,const RRClass & zone_class,const RRsetCollectionBase & zone_rrsets,ZoneCheckerCallbacks & callbacks)152 checkNS(const Name& zone_name, const RRClass& zone_class,
153         const RRsetCollectionBase& zone_rrsets,
154         ZoneCheckerCallbacks& callbacks) {
155     ConstRRsetPtr rrset =
156         zone_rrsets.find(zone_name, zone_class, RRType::NS());
157     if (rrset == NULL) {
158         callbacks.error("zone " + zoneText(zone_name, zone_class) +
159                         ": has no NS records");
160         return;
161     }
162     checkNSNames(zone_name, zone_class, zone_rrsets, rrset, callbacks);
163 }
164 
165 // The following is a simple wrapper of checker callback so checkZone()
166 // can also remember any critical errors.
167 void
errorWrapper(const string & reason,const ZoneCheckerCallbacks * callbacks,bool * had_error)168 errorWrapper(const string& reason, const ZoneCheckerCallbacks* callbacks,
169              bool* had_error) {
170     *had_error = true;
171     callbacks->error(reason);
172 }
173 }
174 
175 bool
checkZone(const Name & zone_name,const RRClass & zone_class,const RRsetCollectionBase & zone_rrsets,const ZoneCheckerCallbacks & callbacks)176 checkZone(const Name& zone_name, const RRClass& zone_class,
177           const RRsetCollectionBase& zone_rrsets,
178           const ZoneCheckerCallbacks& callbacks) {
179     bool had_error = false;
180     ZoneCheckerCallbacks my_callbacks(
181         std::bind(errorWrapper, ph::_1, &callbacks, &had_error),
182         std::bind(&ZoneCheckerCallbacks::warn, &callbacks, ph::_1));
183 
184     checkSOA(zone_name, zone_class, zone_rrsets, my_callbacks);
185     checkNS(zone_name, zone_class, zone_rrsets, my_callbacks);
186 
187     return (!had_error);
188 }
189 
190 } // end namespace dns
191 } // end namespace isc
192