summaryrefslogtreecommitdiffstats
path: root/src/lib/dns/tests/zone_checker_unittest.cc
blob: 863b439eb45b70504d069b39ee96912bd7379005 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
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();
}

}