zone_checker_unittest.cc 14.2 KB
Newer Older
1
// Copyright (C) 2012-2015,2017 Internet Systems Consortium, Inc. ("ISC")
2
//
3
4
5
// 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/.
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

#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/bind.hpp>
#include <boost/scoped_ptr.hpp>

#include <algorithm>
#include <string>
26
#include <sstream>
27
28
29
30
31
32
33
34
#include <vector>

using isc::Unexpected;
using namespace isc::dns;
using namespace isc::dns::rdata;

namespace {

35
36
37
38
39
40
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";

41
42
43
44
class ZoneCheckerTest : public ::testing::Test {
protected:
    ZoneCheckerTest() :
        zname_("example.com"), zclass_(RRClass::IN()),
45
46
        soa_(new RRset(zname_, zclass_, RRType::SOA(), RRTTL(60))),
        ns_(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60))),
47
48
49
        callbacks_(boost::bind(&ZoneCheckerTest::callback, this, _1, true),
                   boost::bind(&ZoneCheckerTest::callback, this, _1, false))
    {
50
51
52
53
54
55
        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_));
56
57
    }

58
public:
59
60
    // This one is passed to boost::bind.  Some compilers seem to require
    // it be public.
61
62
63
64
65
66
67
68
    void callback(const std::string& reason, bool is_error) {
        if (is_error) {
            errors_.push_back(reason);
        } else {
            warns_.push_back(reason);
        }
    }

69
protected:
70
71
72
73
    // 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());
74
75
        for (size_t i = 0;
             i < std::min(expected_errors_.size(), errors_.size());
76
             ++i) {
77
78
79
80
            // 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];
81
82
        }
        EXPECT_EQ(expected_warns_.size(), warns_.size());
83
84
        for (size_t i = 0;
             i < std::min(expected_warns_.size(), warns_.size());
85
             ++i) {
86
87
88
            EXPECT_EQ(0, warns_[0].find(expected_warns_[0]))
                << "actual message: " << warns_[0] << " expected: " <<
                expected_warns_[0];
89
90
91
92
93
94
95
96
97
98
        }

        errors_.clear();
        expected_errors_.clear();
        warns_.clear();
        expected_warns_.clear();
    }

    const Name zname_;
    const RRClass zclass_;
99
    boost::scoped_ptr<RRsetCollection> rrsets_;
100
101
102
103
104
105
106
107
108
109
110
    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.
111
112
113
114
115
116
117
    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));
118
119
120
121
122
123
124
    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.
125
    rrsets_->removeRRset(zname_, zclass_, RRType::SOA());
126
    EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_));
127
    expected_errors_.push_back("zone example.com/IN: has 0 SOA records");
128
129
    checkIssues();

130
131
132
    // If null callback is specified, checkZone() only returns the final
    // result.
    ZoneCheckerCallbacks noerror_callbacks(
133
        0, boost::bind(&ZoneCheckerTest::callback, this, _1, false));
134
135
136
    EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, noerror_callbacks));
    checkIssues();

137
138
    // If there are more than 1 SOA RR, it's also an error.
    errors_.clear();
139
    soa_->addRdata(generic::SOA(soa_txt));
140
141
142
    soa_->addRdata(generic::SOA("ns2.example.com. . 0 0 0 0 0"));
    rrsets_->addRRset(soa_);
    EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_));
143
    expected_errors_.push_back("zone example.com/IN: has 2 SOA records");
144
145
146
147
148
149
150
151
152
153
154
155
156
    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)));
157
    soa_->addRdata(createRdata(RRType::NS(), zclass_, "ns.example.com."));
158
159
160
161
162
163
164
    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.
165
    rrsets_->removeRRset(zname_, zclass_, RRType::NS());
166
    EXPECT_FALSE(checkZone(zname_, zclass_, *rrsets_, callbacks_));
167
    expected_errors_.push_back("zone example.com/IN: has no NS records");
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
    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
}

184
185
186
187
188
189
190
191
192
193
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();

194
195
196
    // Same check, but disabling warning callback.  Same result, but without
    // the warning.
    ZoneCheckerCallbacks nowarn_callbacks(
197
        boost::bind(&ZoneCheckerTest::callback, this, _1, true), 0);
198
199
200
    EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, nowarn_callbacks));
    checkIssues();

201
202
203
204
205
206
207
208
209
210
211
    // 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();

212
    // If there's a CNAME at the name instead, it's an error.
213
    rrsets_->removeRRset(Name("*.example.com"), zclass_, RRType::A());
214
    RRsetPtr cname(new RRset(ns_name, zclass_, RRType::CNAME(), RRTTL(60)));
215
    cname->addRdata(generic::CNAME("cname.example.com."));
216
217
218
219
220
221
    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();

222
    // It doesn't have to be A.  An AAAA is enough.
223
    rrsets_->removeRRset(ns_name, zclass_, RRType::CNAME());
224
225
226
227
228
229
    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();

230
231
232
233
234
235
236
237
238
239
    // 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());
240
241
    rrsets_->removeRRset(zname_, zclass_, RRType::NS());
    ns_.reset(new RRset(zname_, zclass_, RRType::NS(), RRTTL(60)));
242
    ns_->addRdata(generic::NS("ns.example.org."));
243
244
245
246
247
248
249
250
251
252
253
254
255
256
    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();
}

257
258
259
260
261
262
263
264
265
266
267
268
269
270
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)));
271
    child_ns->addRdata(generic::NS("ns.example.org."));
272
273
274
275
276
277
278
    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)));
279
    child_ns->addRdata(generic::NS("ns.example.org."));
280
281
282
283
284
285
286
287
    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)));
288
    child_ns->addRdata(generic::NS("ns.example.org."));
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
    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)));
306
    dname->addRdata(generic::DNAME("example.org."));
307
308
309
310
311
312
313
314
315
316
    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)));
317
    dname->addRdata(generic::DNAME("example.org."));
318
319
320
321
322
323
324
325
326
327
328
    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)));
329
    ns_->addRdata(generic::NS("ns.example.org."));
330
331
332
333
334
335
336
337
338
    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)));
339
    dname->addRdata(generic::DNAME("example.org."));
340
341
342
343
344
345
    rrsets_->addRRset(dname);
    EXPECT_TRUE(checkZone(zname_, zclass_, *rrsets_, callbacks_));
    expected_warns_.push_back("zone example.com/IN: NS has no address");
    checkIssues();
}

346
}