return true;
}
-
-
bool GSQLBackend::removeDomainKey(const ZoneName& name, unsigned int keyId)
{
if(!d_dnssecQueries)
return true;
}
-
bool GSQLBackend::getDomainMetadata(const ZoneName& name, const std::string& kind, std::vector<std::string>& meta)
{
if(!d_dnssecQueries && isDnssecDomainMetadata(kind))
return false;
}
+bool GSQLBackend::get_unsafe(DNSResourceRecord& rec, std::vector<std::pair<std::string, std::string>>& invalid)
+{
+ SSqlStatement::row_t row;
+
+ if ((*d_query_stmt)->hasNextRow()) {
+ try {
+ (*d_query_stmt)->nextRow(row);
+ if (!d_list) {
+ ASSERT_ROW_COLUMNS(d_query_name, row, 8); // lookup(), listSubZone()
+ }
+ else {
+ ASSERT_ROW_COLUMNS(d_query_name, row, 9); // list()
+ }
+ } catch (SSqlException &e) {
+ throw PDNSException("GSQLBackend get: "+e.txtReason());
+ }
+ extractRecord_unsafe(row, rec, invalid);
+ return true;
+ }
+
+ try {
+ (*d_query_stmt)->reset();
+ } catch (SSqlException &e) {
+ throw PDNSException("GSQLBackend get: "+e.txtReason());
+ }
+ d_query_stmt = nullptr;
+ return false;
+}
+
bool GSQLBackend::autoPrimaryAdd(const AutoPrimary& primary)
{
try{
throw PDNSException("GSQLBackend unable to insert an autoprimary with IP " + primary.ip + " and nameserver name '" + primary.nameserver + "' and account '" + primary.account + "': " + e.txtReason());
}
return true;
-
}
bool GSQLBackend::autoPrimaryRemove(const AutoPrimary& primary)
throw PDNSException("GSQLBackend unable to remove an autoprimary with IP " + primary.ip + " and nameserver name '" + primary.nameserver + "': " + e.txtReason());
}
return true;
-
}
bool GSQLBackend::autoPrimariesList(std::vector<AutoPrimary>& primaries)
}
}
+// Similar logic to extractRecord() above, but returns the invalid field names
+// and their contents.
+void GSQLBackend::extractRecord_unsafe(SSqlStatement::row_t& row, DNSResourceRecord& rec, std::vector<std::pair<std::string, std::string>>& invalid)
+{
+ invalid.clear();
+
+ try {
+ static const int defaultTTL = ::arg().asNum( "default-ttl" );
+
+ if (row[1].empty()) {
+ rec.ttl = defaultTTL;
+ }
+ else {
+ pdns::checked_stoi_into(rec.ttl, row[1]);
+ }
+ }
+ catch (...) {
+ invalid.emplace_back(std::make_pair("ttl", row[1]));
+ rec.ttl = 0;
+ }
+
+ try {
+ if (!d_qname.empty()) {
+ rec.qname = d_qname;
+ }
+ else {
+ rec.qname = DNSName(row[6]);
+ }
+ }
+ catch (...) {
+ invalid.emplace_back(std::make_pair("qname", row[6]));
+ rec.qname.clear();
+ }
+
+ rec.qtype=row[3];
+
+ try {
+ if (d_upgradeContent && DNSRecordContent::isUnknownType(row[3]) && DNSRecordContent::isRegisteredType(rec.qtype, rec.qclass)) {
+ rec.content = DNSRecordContent::upgradeContent(rec.qname, rec.qtype, row[0]);
+ }
+ else if (rec.qtype==QType::MX || rec.qtype==QType::SRV) {
+ rec.content.reserve(row[2].size() + row[0].size() + 1);
+ rec.content=row[2]+" "+row[0];
+ }
+ else {
+ rec.content=std::move(row[0]);
+ }
+ }
+ catch (...) {
+ invalid.emplace_back(std::make_pair("content", row[0]));
+ rec.content.clear();
+ }
+
+ rec.last_modified=0;
+
+ if (d_dnssecQueries) {
+ rec.auth = !row[7].empty() && row[7][0]=='1';
+ }
+ else {
+ rec.auth = true;
+ }
+
+ rec.disabled = !row[5].empty() && row[5][0]=='1';
+
+ try {
+ pdns::checked_stoi_into(rec.domain_id, row[4]);
+ }
+ catch (...) {
+ invalid.emplace_back(std::make_pair("domain_id", row[4]));
+ rec.domain_id = 0;
+ }
+
+ if (row.size() > 8) { // if column 8 exists, it holds an ordername
+ try {
+ if (!row.at(8).empty()) {
+ rec.ordername=DNSName(boost::replace_all_copy(row.at(8), " ", ".")).labelReverse();
+ }
+ else {
+ rec.ordername.clear();
+ }
+ }
+ catch (...) {
+ invalid.emplace_back(std::make_pair("ordername", row.at(8)));
+ rec.ordername.clear();
+ }
+ }
+ else {
+ rec.ordername.clear();
+ }
+}
+
void GSQLBackend::extractComment(SSqlStatement::row_t& row, Comment& comment)
{
pdns::checked_stoi_into(comment.domain_id, row[0]);
string directBackendCmd(const string &query) override;
bool searchRecords(const string &pattern, size_t maxResults, vector<DNSResourceRecord>& result) override;
bool searchComments(const string &pattern, size_t maxResults, vector<Comment>& result) override;
+ bool get_unsafe(DNSResourceRecord& rec, std::vector<std::pair<std::string, std::string>>& invalid) override;
protected:
string pattern2SQLPattern(const string& pattern);
void extractRecord(SSqlStatement::row_t& row, DNSResourceRecord& rr);
+ void extractRecord_unsafe(SSqlStatement::row_t& row, DNSResourceRecord& rec, std::vector<std::pair<std::string, std::string>>& invalid);
void extractComment(SSqlStatement::row_t& row, Comment& c);
void setLastCheck(domainid_t domain_id, time_t lastcheck);
bool isConnectionUsable() {
const string& getPrefix() { return d_prefix; };
+ // The following routine allows callers to retrieve invalid records from
+ // the backend, which would not be returned by get(). This is used by
+ // pdnsutil for diagnostic purposes.
+ // The default implementation simply wraps get() and pretends there are
+ // no invalid or corrupted records in the backend storage.
+
+ virtual bool get_unsafe(DNSResourceRecord& rec, std::vector<std::pair<std::string, std::string>>& invalid)
+ {
+ invalid.clear();
+ return get(rec);
+ }
+
protected:
bool mustDo(const string& key);
const string& getArg(const string& key);
return result;
}
+// Returns a terminal-friendly version of its input, with non-printable
+// characters replaced with hex sequences.
+// Filters fewer printable characters than makeLuaString().
+static std::string terminalSafe(const std::string& input)
+{
+ size_t toRewrite{0};
+ for (const auto& chr : input) {
+ if (::isprint(static_cast<int>(chr)) == 0) {
+ ++toRewrite;
+ }
+ }
+ if (toRewrite == 0) {
+ return input;
+ }
+ std::string output;
+ std::array<char, 5> tmp{};
+ output.reserve(input.size() + 3 * toRewrite);
+ for (const auto& chr : input) {
+ if (::isprint(static_cast<int>(chr)) != 0) {
+ output.push_back(chr);
+ }
+ else {
+ switch (chr) {
+ case '\n':
+ output.append("\\n");
+ break;
+ case '\r':
+ output.append("\\r");
+ break;
+ case '\t':
+ output.append("\\t");
+ break;
+ default:
+ snprintf(tmp.data(), tmp.size(), "\\x%02x", static_cast<int>(chr));
+ output.append(tmp.data());
+ }
+ }
+ }
+ return output;
+}
+
static int checkZone(DNSSECKeeper &dk, UeberBackend &B, const ZoneName& zone, const vector<DNSResourceRecord>* suppliedrecords=nullptr) // NOLINT(readability-function-cognitive-complexity,readability-identifier-length)
{
int numerrors=0;
vector<DNSResourceRecord> records;
if(suppliedrecords == nullptr) {
+ std::vector<std::pair<std::string, std::string>> invalid;
DNSResourceRecord drr;
sd.db->list(zone, sd.domain_id, g_verbose);
- while(sd.db->get(drr)) {
- records.push_back(drr);
+ while (sd.db->get_unsafe(drr, invalid)) {
+ if (invalid.empty()) {
+ records.push_back(drr);
+ continue;
+ }
+ // Emit this alert as a warning, as this is not something which pdnsutil
+ // can fix by itself, and that record will be silently ignored during
+ // regular operation.
+ cout << "[Warning] Ill-formed ";
+ // The invalid part might be the record name itself, only output it if
+ // non-empty.
+ if (!drr.qname.empty()) {
+ cout << "'" << drr.qname << "' ";
+ }
+ cout << "record in backend storage: ";
+ bool first = true;
+ for (const auto& pair : invalid) {
+ if (first) {
+ first = false;
+ }
+ else {
+ cout << ", ";
+ }
+ cout << "field " << pair.first << " has invalid content '" << terminalSafe(pair.second) << "'";
+ }
+ cout << std::endl;
+ numwarnings++;
}
}
else
<< endl;
for (const auto& group : topLevelDispatcher) {
if (!group.second.first) { // toplevel synonyms (sugar), don't list
- continue;
+ continue;
}
for (const auto& dispatcher : group.second.second) {
displayCommandGroup(dispatcher, group.first);
--- /dev/null
+#!/usr/bin/env bash
+
+## source ../regression-tests/common
+
+rm -f pdns-gsqlite3.conf pdns.sqlite3
+
+cat > pdns-gsqlite3.conf << __EOF__
+launch=gsqlite3
+gsqlite3-database=pdns.sqlite3
+module-dir=../regression-tests/modules
+__EOF__
+
+ARGS="--config-dir=. --config-name=gsqlite3"
+
+sqlite3 pdns.sqlite3 < ../modules/gsqlite3backend/schema.sqlite3.sql
+tosql gsqlite | sqlite3 pdns.sqlite3
+echo ANALYZE\; | sqlite3 pdns.sqlite3
+
+# Create zone
+$PDNSUTIL $ARGS zone create bug.less
+
+# Add some valid records
+$PDNSUTIL $ARGS rrset add bug.less bug.less NS no.bug.less
+$PDNSUTIL $ARGS rrset add bug.less no.bug.less A 1.2.3.4
+$PDNSUTIL $ARGS rrset add bug.less never.bug.less A 1.2.3.4
+
+# Alter some records to make their contents invalid
+echo "UPDATE records SET name='this-name-turns-out-to-be-an-invalid-dns-name-because-it-is-way-larger-than-sixty-three-characters-and-this-is-not-allowed-for-labels.bug.less' WHERE name='no.bug.less';" | sqlite3 pdns.sqlite3 2>&1
+echo "UPDATE records SET ordername='..' WHERE name='never.bug.less';" | sqlite3 pdns.sqlite3 2>&1
+
+# Check that pdnsutil zone list doesn't show the invalid records
+$PDNSUTIL $ARGS zone list bug.less
+
+# Check that pdnsutil zone check reports the invalid records
+$PDNSUTIL $ARGS zone check bug.less
+
+exit 0
--- /dev/null
+This test checks that an intentionally crafted incorrect record gets ignored
+during regular lookups and reported as incorrect by pdnsutil zone check.
--- /dev/null
+New rrset:
+bug.less. 3600 IN NS no.bug.less
+New rrset:
+no.bug.less. 3600 IN A 1.2.3.4
+New rrset:
+never.bug.less. 3600 IN A 1.2.3.4
+$ORIGIN .
+bug.less 3600 IN NS no.bug.less.
+bug.less 3600 IN SOA a.misconfigured.dns.server.invalid hostmaster.bug.less 0 10800 3600 604800 3600
+[Warning] Ill-formed 'never.bug.less' record in backend storage: field ordername has invalid content '..'
+[Warning] Ill-formed record in backend storage: field qname has invalid content 'this-name-turns-out-to-be-an-invalid-dns-name-because-it-is-way-larger-than-sixty-three-characters-and-this-is-not-allowed-for-labels.bug.less'
+Checked 2 records of 'bug.less', 0 errors, 2 warnings.