From 1e3de951b1efd96ea18d6671b23b734069337b2b Mon Sep 17 00:00:00 2001 From: Gary Lockyer Date: Tue, 25 Jul 2017 14:14:53 +1200 Subject: [PATCH 1/7] dnsserver: Tests for dns wildcard entries Add tests for dns wildcards. Tests validated against Windows Server 2012 R2 Signed-off-by: Gary Lockyer Reviewed-by: Andrew Bartlett Reviewed-by: Garming Sam BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952 (cherry picked from commit 1184770a76800897d59f1c20adaee3b0240697de) --- python/samba/tests/dns_wildcard.py | 288 +++++++++++++++++++++++++++++++++++++ selftest/knownfail.d/dns_wildcard | 2 + source4/selftest/tests.py | 2 +- 3 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 python/samba/tests/dns_wildcard.py create mode 100644 selftest/knownfail.d/dns_wildcard diff --git a/python/samba/tests/dns_wildcard.py b/python/samba/tests/dns_wildcard.py new file mode 100644 index 0000000..ca8426a --- /dev/null +++ b/python/samba/tests/dns_wildcard.py @@ -0,0 +1,288 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Andrew Bartlett 2007 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import sys +from samba import credentials +from samba.dcerpc import dns, dnsserver +from samba.netcmd.dns import data_to_dns_record +from samba.tests.subunitrun import SubunitOptions, TestProgram +from samba import werror, WERRORError +from samba.tests.dns_base import DNSTest +import samba.getopt as options +import optparse + +parser = optparse.OptionParser( + "dns_wildcard.py [options]") +sambaopts = options.SambaOptions(parser) +parser.add_option_group(sambaopts) + +# This timeout only has relevance when testing against Windows +# Format errors tend to return patchy responses, so a timeout is needed. +parser.add_option("--timeout", type="int", dest="timeout", + help="Specify timeout for DNS requests") + +# To run against Windows +# python python/samba/tests/dns_wildcard.py computer_name ip +# -U"Administrator%admin_password" +# --realm=Domain_name +# --timeout 10 +# + +# use command line creds if available +credopts = options.CredentialsOptions(parser) +parser.add_option_group(credopts) +subunitopts = SubunitOptions(parser) +parser.add_option_group(subunitopts) + +opts, args = parser.parse_args() + +lp = sambaopts.get_loadparm() +creds = credopts.get_credentials(lp) + +timeout = opts.timeout + +if len(args) < 2: + parser.print_usage() + sys.exit(1) + +server_name = args[0] +server_ip = args[1] +creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE) + +WILDCARD_IP = "1.1.1.1" +WILDCARD = "*.wildcardtest" +EXACT_IP = "1.1.1.2" +EXACT = "exact.wildcardtest" +LEVEL2_WILDCARD_IP = "1.1.1.3" +LEVEL2_WILDCARD = "*.level2.wildcardtest" +LEVEL2_EXACT_IP = "1.1.1.4" +LEVEL2_EXACT = "exact.level2.wildcardtest" + + +class TestWildCardQueries(DNSTest): + + def setUp(self): + super(TestWildCardQueries, self).setUp() + global server, server_ip, lp, creds, timeout + self.server = server_name + self.server_ip = server_ip + self.lp = lp + self.creds = creds + self.timeout = timeout + + # Create the dns records + self.dns_records = [(dns.DNS_QTYPE_A, + "%s.%s" % (WILDCARD, self.get_dns_domain()), + WILDCARD_IP), + (dns.DNS_QTYPE_A, + "%s.%s" % (EXACT, self.get_dns_domain()), + EXACT_IP), + (dns.DNS_QTYPE_A, + ("%s.%s" % ( + LEVEL2_WILDCARD, + self.get_dns_domain())), + LEVEL2_WILDCARD_IP), + (dns.DNS_QTYPE_A, + ("%s.%s" % ( + LEVEL2_EXACT, + self.get_dns_domain())), + LEVEL2_EXACT_IP)] + + c = self.dns_connect() + for (typ, name, data) in self.dns_records: + self.add_record(c, typ, name, data) + + def tearDown(self): + c = self.dns_connect() + for (typ, name, data) in self.dns_records: + self.delete_record(c, typ, name, data) + + def dns_connect(self): + binding_str = "ncacn_ip_tcp:%s[sign]" % self.server_ip + return dnsserver.dnsserver(binding_str, self.lp, self.creds) + + def delete_record(self, dns_conn, typ, name, data): + + rec = data_to_dns_record(typ, data) + del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + del_rec_buf.rec = rec + + try: + dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN, + 0, + self.server, + self.get_dns_domain(), + name, + None, + del_rec_buf) + except WERRORError as e: + # Ignore record does not exist errors + if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST: + raise e + + def add_record(self, dns_conn, typ, name, data): + + rec = data_to_dns_record(typ, data) + add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() + add_rec_buf.rec = rec + try: + dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN, + 0, + self.server, + self.get_dns_domain(), + name, + add_rec_buf, + None) + except WERRORError as e: + raise e + + def test_one_a_query_match_wildcard(self): + "Query an A record, should match the wildcard entry" + + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + # Check the record + name = "miss.wildcardtest.%s" % self.get_dns_domain() + q = self.make_name_question(name, + dns.DNS_QTYPE_A, + dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + (response, response_packet) =\ + self.dns_transaction_udp(p, host=self.server_ip) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rr_type, dns.DNS_QTYPE_A) + self.assertEquals(response.answers[0].rdata, WILDCARD_IP) + + def test_one_a_query_wildcard_entry(self): + "Query the wildcard entry" + + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + # Check the record + name = "%s.%s" % (WILDCARD, self.get_dns_domain()) + q = self.make_name_question(name, + dns.DNS_QTYPE_A, + dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + (response, response_packet) =\ + self.dns_transaction_udp(p, host=self.server_ip) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rr_type, dns.DNS_QTYPE_A) + self.assertEquals(response.answers[0].rdata, WILDCARD_IP) + + def test_one_a_query_exact_match(self): + """Query an entry that matches the wild card but has an exact match as + well. + """ + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + # Check the record + name = "%s.%s" % (EXACT, self.get_dns_domain()) + q = self.make_name_question(name, + dns.DNS_QTYPE_A, + dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + (response, response_packet) =\ + self.dns_transaction_udp(p, host=self.server_ip) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rr_type, dns.DNS_QTYPE_A) + self.assertEquals(response.answers[0].rdata, EXACT_IP) + + def test_one_a_query_match_wildcard_l2(self): + "Query an A record, should match the level 2 wildcard entry" + + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + # Check the record + name = "miss.level2.wildcardtest.%s" % self.get_dns_domain() + q = self.make_name_question(name, + dns.DNS_QTYPE_A, + dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + (response, response_packet) =\ + self.dns_transaction_udp(p, host=self.server_ip) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rr_type, dns.DNS_QTYPE_A) + self.assertEquals(response.answers[0].rdata, LEVEL2_WILDCARD_IP) + + def test_one_a_query_exact_match_l2(self): + """Query an entry that matches the wild card but has an exact match as + well. + """ + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + # Check the record + name = "%s.%s" % (LEVEL2_EXACT, self.get_dns_domain()) + q = self.make_name_question(name, + dns.DNS_QTYPE_A, + dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + (response, response_packet) =\ + self.dns_transaction_udp(p, host=self.server_ip) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rr_type, dns.DNS_QTYPE_A) + self.assertEquals(response.answers[0].rdata, LEVEL2_EXACT_IP) + + def test_one_a_query_wildcard_entry_l2(self): + "Query the level 2 wildcard entry" + + p = self.make_name_packet(dns.DNS_OPCODE_QUERY) + questions = [] + + # Check the record + name = "%s.%s" % (LEVEL2_WILDCARD, self.get_dns_domain()) + q = self.make_name_question(name, + dns.DNS_QTYPE_A, + dns.DNS_QCLASS_IN) + questions.append(q) + + self.finish_name_packet(p, questions) + (response, response_packet) =\ + self.dns_transaction_udp(p, host=self.server_ip) + self.assert_dns_rcode_equals(response, dns.DNS_RCODE_OK) + self.assert_dns_opcode_equals(response, dns.DNS_OPCODE_QUERY) + self.assertEquals(response.ancount, 1) + self.assertEquals(response.answers[0].rr_type, dns.DNS_QTYPE_A) + self.assertEquals(response.answers[0].rdata, LEVEL2_WILDCARD_IP) + + +TestProgram(module=__name__, opts=subunitopts) diff --git a/selftest/knownfail.d/dns_wildcard b/selftest/knownfail.d/dns_wildcard new file mode 100644 index 0000000..7e7892f --- /dev/null +++ b/selftest/knownfail.d/dns_wildcard @@ -0,0 +1,2 @@ +^samba.tests.dns_wildcard.__main__.TestWildCardQueries.test_one_a_query_match_wildcard\(ad_dc\) +^samba.tests.dns_wildcard.__main__.TestWildCardQueries.test_one_a_query_match_wildcard_l2\(ad_dc\) diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index 4bcbdc6..012f87a 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -367,7 +367,7 @@ plantestsuite_loadlist("samba.tests.dns", "vampire_dc:local", [python, os.path.j plantestsuite_loadlist("samba.tests.dns_forwarder", "fl2003dc:local", [python, os.path.join(srcdir(), "python/samba/tests/dns_forwarder.py"), '$SERVER', '$SERVER_IP', '$DNS_FORWARDER1', '$DNS_FORWARDER2', '--machine-pass', '-U"$USERNAME%$PASSWORD"', '--workgroup=$DOMAIN', '$LOADLIST', '$LISTOPT']) plantestsuite_loadlist("samba.tests.dns_tkey", "fl2008r2dc", [python, os.path.join(srcdir(), "python/samba/tests/dns_tkey.py"), '$SERVER', '$SERVER_IP', '--machine-pass', '-U"$USERNAME%$PASSWORD"', '--workgroup=$DOMAIN', '$LOADLIST', '$LISTOPT']) - +plantestsuite_loadlist("samba.tests.dns_wildcard", "ad_dc", [python, os.path.join(srcdir(), "python/samba/tests/dns_wildcard.py"), '$SERVER', '$SERVER_IP', '--machine-pass', '-U"$USERNAME%$PASSWORD"', '--workgroup=$DOMAIN', '$LOADLIST', '$LISTOPT']) for t in smbtorture4_testsuites("dns_internal."): plansmbtorture4testsuite(t, "ad_dc_ntvfs:local", '//$SERVER/whavever') -- 1.9.1 From a3caf90b255d789c59c1630d52fbe6b122797e24 Mon Sep 17 00:00:00 2001 From: Gary Lockyer Date: Thu, 3 Aug 2017 15:12:51 +1200 Subject: [PATCH 2/7] dnsserver: Tighten DNS name checking Add checks for the maximum permitted length, maximum number of labels and the maximum label length. These extra checks will be used by the DNS wild card handling. Signed-off-by: Gary Lockyer Reviewed-by: Andrew Bartlett Reviewed-by: Garming Sam BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952 (cherry picked from commit 34acf5a99214639e5e7792a9e85d24c9fd7640ac) --- librpc/idl/dns.idl | 3 +++ source4/dns_server/dnsserver_common.c | 35 +++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/librpc/idl/dns.idl b/librpc/idl/dns.idl index aebb106..8e8eed5 100644 --- a/librpc/idl/dns.idl +++ b/librpc/idl/dns.idl @@ -18,6 +18,9 @@ import "misc.idl", "dnsp.idl"; interface dns { const int DNS_SERVICE_PORT = 53; + const int DNS_MAX_LABELS = 127; + const int DNS_MAX_DOMAIN_LENGTH = 253; + const int DNS_MAX_LABEL_LENGTH = 63; typedef [public,bitmap16bit] bitmap { DNS_RCODE = 0x000F, diff --git a/source4/dns_server/dnsserver_common.c b/source4/dns_server/dnsserver_common.c index a56ff08..2a81b83 100644 --- a/source4/dns_server/dnsserver_common.c +++ b/source4/dns_server/dnsserver_common.c @@ -246,25 +246,48 @@ static int rec_cmp(const struct dnsp_DnssrvRpcRecord *r1, } /* - * Check for valid DNS names. These are names which are non-empty, do not - * start with a dot and do not have any empty segments. + * Check for valid DNS names. These are names which: + * - are non-empty + * - do not start with a dot + * - do not have any empty labels + * - have no more than 127 labels + * - are no longer than 253 characters + * - none of the labels exceed 63 characters */ WERROR dns_name_check(TALLOC_CTX *mem_ctx, size_t len, const char *name) { size_t i; + unsigned int labels = 0; + unsigned int label_len = 0; if (len == 0) { return WERR_DS_INVALID_DN_SYNTAX; } + if (len > 1 && name[0] == '.') { + return WERR_DS_INVALID_DN_SYNTAX; + } + + if ((len - 1) > DNS_MAX_DOMAIN_LENGTH) { + return WERR_DS_INVALID_DN_SYNTAX; + } + for (i = 0; i < len - 1; i++) { if (name[i] == '.' && name[i+1] == '.') { return WERR_DS_INVALID_DN_SYNTAX; } - } - - if (len > 1 && name[0] == '.') { - return WERR_DS_INVALID_DN_SYNTAX; + if (name[i] == '.') { + labels++; + if (labels > DNS_MAX_LABELS) { + return WERR_DS_INVALID_DN_SYNTAX; + } + label_len = 0; + } else { + label_len++; + if (label_len > DNS_MAX_LABEL_LENGTH) { + return WERR_DS_INVALID_DN_SYNTAX; + } + } } return WERR_OK; -- 1.9.1 From eae4c479d1dfcc7ed10001c133941c657787032b Mon Sep 17 00:00:00 2001 From: Gary Lockyer Date: Thu, 3 Aug 2017 15:12:02 +1200 Subject: [PATCH 3/7] dnsserver: Add support for dns wildcards Add support for dns wildcard records. i.e. if the following records exist exact.samba.example.com 3600 A 1.1.1.1 *.samba.example.com 3600 A 1.1.1.2 look up on exact.samba.example.com will return 1.1.1.1 look up on *.samba.example.com will return 1.1.1.2 look up on other.samba.example.com will return 1.1.1.2 Signed-off-by: Gary Lockyer Reviewed-by: Andrew Bartlett Reviewed-by: Garming Sam BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952 (cherry picked from commit edcbc991253f4d6f59ef9a43a691c66cbbdc2b6d) --- selftest/knownfail.d/dns_wildcard | 2 - source4/dns_server/dlz_bind9.c | 4 +- source4/dns_server/dns_query.c | 5 +- source4/dns_server/dns_server.h | 5 + source4/dns_server/dns_utils.c | 18 ++ source4/dns_server/dnsserver_common.c | 348 ++++++++++++++++++++++++++++++++++ source4/dns_server/dnsserver_common.h | 5 + 7 files changed, 380 insertions(+), 7 deletions(-) delete mode 100644 selftest/knownfail.d/dns_wildcard diff --git a/selftest/knownfail.d/dns_wildcard b/selftest/knownfail.d/dns_wildcard deleted file mode 100644 index 7e7892f..0000000 --- a/selftest/knownfail.d/dns_wildcard +++ /dev/null @@ -1,2 +0,0 @@ -^samba.tests.dns_wildcard.__main__.TestWildCardQueries.test_one_a_query_match_wildcard\(ad_dc\) -^samba.tests.dns_wildcard.__main__.TestWildCardQueries.test_one_a_query_match_wildcard_l2\(ad_dc\) diff --git a/source4/dns_server/dlz_bind9.c b/source4/dns_server/dlz_bind9.c index 7096f47..6ef378c 100644 --- a/source4/dns_server/dlz_bind9.c +++ b/source4/dns_server/dlz_bind9.c @@ -865,8 +865,8 @@ static isc_result_t dlz_lookup_types(struct dlz_bind9_data *state, return ISC_R_NOMEMORY; } - werr = dns_common_lookup(state->samdb, tmp_ctx, dn, - &records, &num_records, NULL); + werr = dns_common_wildcard_lookup(state->samdb, tmp_ctx, dn, + &records, &num_records); if (W_ERROR_IS_OK(werr)) { break; } diff --git a/source4/dns_server/dns_query.c b/source4/dns_server/dns_query.c index b8ecc2e..e8de304 100644 --- a/source4/dns_server/dns_query.c +++ b/source4/dns_server/dns_query.c @@ -625,9 +625,8 @@ static struct tevent_req *handle_authoritative_send( if (tevent_req_werror(req, werr)) { return tevent_req_post(req, ev); } - - werr = dns_lookup_records(dns, state, dn, &state->recs, - &state->rec_count); + werr = dns_lookup_records_wildcard(dns, state, dn, &state->recs, + &state->rec_count); TALLOC_FREE(dn); if (tevent_req_werror(req, werr)) { return tevent_req_post(req, ev); diff --git a/source4/dns_server/dns_server.h b/source4/dns_server/dns_server.h index 5395ff9..382b6bd 100644 --- a/source4/dns_server/dns_server.h +++ b/source4/dns_server/dns_server.h @@ -95,6 +95,11 @@ WERROR dns_lookup_records(struct dns_server *dns, struct ldb_dn *dn, struct dnsp_DnssrvRpcRecord **records, uint16_t *rec_count); +WERROR dns_lookup_records_wildcard(struct dns_server *dns, + TALLOC_CTX *mem_ctx, + struct ldb_dn *dn, + struct dnsp_DnssrvRpcRecord **records, + uint16_t *rec_count); WERROR dns_replace_records(struct dns_server *dns, TALLOC_CTX *mem_ctx, struct ldb_dn *dn, diff --git a/source4/dns_server/dns_utils.c b/source4/dns_server/dns_utils.c index c728eaa..ee35bd2 100644 --- a/source4/dns_server/dns_utils.c +++ b/source4/dns_server/dns_utils.c @@ -107,6 +107,10 @@ bool dns_records_match(struct dnsp_DnssrvRpcRecord *rec1, return false; } +/* + * Lookup a DNS record, performing an exact match. + * i.e. DNS wild card records are not considered. + */ WERROR dns_lookup_records(struct dns_server *dns, TALLOC_CTX *mem_ctx, struct ldb_dn *dn, @@ -117,6 +121,20 @@ WERROR dns_lookup_records(struct dns_server *dns, records, rec_count, NULL); } +/* + * Lookup a DNS record, will match DNS wild card records if an exact match + * is not found. + */ +WERROR dns_lookup_records_wildcard(struct dns_server *dns, + TALLOC_CTX *mem_ctx, + struct ldb_dn *dn, + struct dnsp_DnssrvRpcRecord **records, + uint16_t *rec_count) +{ + return dns_common_wildcard_lookup(dns->samdb, mem_ctx, dn, + records, rec_count); +} + WERROR dns_replace_records(struct dns_server *dns, TALLOC_CTX *mem_ctx, struct ldb_dn *dn, diff --git a/source4/dns_server/dnsserver_common.c b/source4/dns_server/dnsserver_common.c index 2a81b83..217e65b 100644 --- a/source4/dns_server/dnsserver_common.c +++ b/source4/dns_server/dnsserver_common.c @@ -133,6 +133,10 @@ WERROR dns_common_extract(struct ldb_context *samdb, return WERR_OK; } +/* + * Lookup a DNS record, performing an exact match. + * i.e. DNS wild card records are not considered. + */ WERROR dns_common_lookup(struct ldb_context *samdb, TALLOC_CTX *mem_ctx, struct ldb_dn *dn, @@ -229,6 +233,350 @@ WERROR dns_common_lookup(struct ldb_context *samdb, return WERR_OK; } +/* + * Build an ldb_parse_tree node for an equality check + * + * Note: name is assumed to have been validated by dns_name_check + * so will be zero terminated and of a reasonable size. + */ +static struct ldb_parse_tree *build_equality_operation( + TALLOC_CTX *mem_ctx, + bool add_asterix, /* prepend an '*' to the name */ + const uint8_t *name, /* the value being matched */ + const char *attr, /* the attribute to check name against */ + size_t size) /* length of name */ +{ + + struct ldb_parse_tree *el = NULL; /* Equality node being built */ + struct ldb_val *value = NULL; /* Value the attr will be compared + with */ + size_t length = 0; /* calculated length of the value + including option '*' prefix and + '\0' string terminator */ + + el = talloc(mem_ctx, struct ldb_parse_tree); + if (el == NULL) { + DBG_ERR("Unable to allocate ldb_parse_tree\n"); + return NULL; + } + + el->operation = LDB_OP_EQUALITY; + el->u.equality.attr = talloc_strdup(mem_ctx, attr); + value = &el->u.equality.value; + length = (add_asterix) ? size + 2 : size + 1; + value->data = talloc_zero_array(el, uint8_t, length); + if (el == NULL) { + DBG_ERR("Unable to allocate value->data\n"); + TALLOC_FREE(el); + return NULL; + } + + value->length = length; + if (add_asterix) { + value->data[0] = '*'; + memcpy(&value->data[1], name, size); + } else { + memcpy(value->data, name, size); + } + return el; +} + +/* + * Determine the number of levels in name + * essentially the number of '.'s in the name + 1 + * + * name is assumed to have been validated by dns_name_check + */ +static unsigned int number_of_labels(const struct ldb_val *name) { + int x = 0; + unsigned int labels = 1; + for (x = 0; x < name->length; x++) { + if (name->data[x] == '.') { + labels++; + } + } + return labels; +} +/* + * Build a query that matches the target name, and any possible + * DNS wild card entries + * + * Builds a parse tree equivalent to the example query. + * + * x.y.z -> (|(name=x.y.z)(name=\2a.y.z)(name=\2a.z)(name=\2a)) + * + * Returns NULL if unable to build the query. + * + * The first component of the DN is assumed to be the name being looked up + * and also that it has been validated by dns_name_check + * + */ +#define BASE "(&(objectClass=dnsNode)(!(dNSTombstoned=TRUE))(|(a=b)(c=d)))" +static struct ldb_parse_tree *build_wildcard_query( + TALLOC_CTX *mem_ctx, + struct ldb_dn *dn) +{ + const struct ldb_val *name = NULL; /* The DNS name being + queried */ + const char *attr = NULL; /* The attribute name */ + struct ldb_parse_tree *query = NULL; /* The constructed query + parse tree*/ + struct ldb_parse_tree *wildcard_query = NULL; /* The parse tree for the + name and wild card + entries */ + int labels = 0; /* The number of labels in the name */ + + attr = ldb_dn_get_rdn_name(dn); + if (attr == NULL) { + DBG_ERR("Unable to get rdn_name\n"); + return NULL; + } + + name = ldb_dn_get_rdn_val(dn); + if (name == NULL) { + DBG_ERR("Unable to get domain name value\n"); + return NULL; + } + labels = number_of_labels(name); + + query = ldb_parse_tree(mem_ctx, BASE); + if (query == NULL) { + DBG_ERR("Unable to parse query %s\n", BASE); + return NULL; + } + + /* + * The 3rd element of BASE is a place holder which is replaced with + * the actual wild card query + */ + wildcard_query = query->u.list.elements[2]; + TALLOC_FREE(wildcard_query->u.list.elements); + + wildcard_query->u.list.num_elements = labels + 1; + wildcard_query->u.list.elements = talloc_array( + wildcard_query, + struct ldb_parse_tree *, + labels + 1); + /* + * Build the wild card query + */ + { + int x = 0; /* current character in the name */ + int l = 0; /* current equality operator index in elements */ + struct ldb_parse_tree *el = NULL; /* Equality operator being + built */ + bool add_asterix = true; /* prepend an '*' to the value */ + for (l = 0, x = 0; l < labels && x < name->length; l++) { + unsigned int size = name->length - x; + add_asterix = (name->data[x] == '.'); + el = build_equality_operation( + mem_ctx, + add_asterix, + &name->data[x], + attr, + size); + if (el == NULL) { + return NULL; /* Reason will have been logged */ + } + wildcard_query->u.list.elements[l] = el; + + /* skip to the start of the next label */ + for (;x < name->length && name->data[x] != '.'; x++); + } + + /* Add the base level "*" only query */ + el = build_equality_operation(mem_ctx, true, NULL, attr, 0); + if (el == NULL) { + TALLOC_FREE(query); + return NULL; /* Reason will have been logged */ + } + wildcard_query->u.list.elements[l] = el; + } + return query; +} + +/* + * Scan the list of records matching a dns wildcard query and return the + * best match. + * + * The best match is either an exact name match, or the longest wild card + * entry returned + * + * i.e. name = a.b.c candidates *.b.c, *.c, - *.b.c would be selected + * name = a.b.c candidates a.b.c, *.b.c, *.c - a.b.c would be selected + */ +static struct ldb_message *get_best_match(struct ldb_dn *dn, + struct ldb_result *result) +{ + int matched = 0; /* Index of the current best match in result */ + size_t length = 0; /* The length of the current candidate */ + const struct ldb_val *target = NULL; /* value we're looking for */ + const struct ldb_val *candidate = NULL; /* current candidate value */ + int x = 0; + + target = ldb_dn_get_rdn_val(dn); + for(x = 0; x < result->count; x++) { + candidate = ldb_dn_get_rdn_val(result->msgs[x]->dn); + if (strncasecmp((char *) target->data, + (char *) candidate->data, + target->length) == 0) { + /* Exact match stop searching and return */ + return result->msgs[x]; + } + if (candidate->length > length) { + matched = x; + length = candidate->length; + } + } + return result->msgs[matched]; +} + +/* + * Look up a DNS entry, if an exact match does not exist, return the + * closest matching DNS wildcard entry if available + * + * Returns: LDB_ERR_NO_SUCH_OBJECT If no matching record exists + * LDB_ERR_OPERATIONS_ERROR If the query fails + * LDB_SUCCESS If a matching record was retrieved + * + */ +static int dns_wildcard_lookup(struct ldb_context *samdb, + TALLOC_CTX *mem_ctx, + struct ldb_dn *dn, + struct ldb_message **msg) +{ + static const char * const attrs[] = { + "dnsRecord", + "dNSTombstoned", + NULL + }; + struct ldb_dn *parent = NULL; /* The parent dn */ + struct ldb_result *result = NULL; /* Results of the search */ + int ret; /* Return code */ + struct ldb_parse_tree *query = NULL; /* The query to run */ + struct ldb_request *request = NULL; /* LDB request for the query op */ + struct ldb_message *match = NULL; /* the best matching DNS record */ + TALLOC_CTX *frame = talloc_stackframe(); + + parent = ldb_dn_get_parent(frame, dn); + if (parent == NULL) { + DBG_ERR("Unable to extract parent from dn\n"); + TALLOC_FREE(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + + query = build_wildcard_query(frame, dn); + if (query == NULL) { + TALLOC_FREE(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + + result = talloc_zero(mem_ctx, struct ldb_result); + if (result == NULL) { + TALLOC_FREE(frame); + DBG_ERR("Unable to allocate ldb_result\n"); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = ldb_build_search_req_ex(&request, + samdb, + frame, + parent, + LDB_SCOPE_ONELEVEL, + query, + attrs, + NULL, + result, + ldb_search_default_callback, + NULL); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(frame); + DBG_ERR("ldb_build_search_req_ex returned %d\n", ret); + return ret; + } + + ret = ldb_request(samdb, request); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(frame); + return ret; + } + + ret = ldb_wait(request->handle, LDB_WAIT_ALL); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(frame); + return ret; + } + + if (result->count == 0) { + TALLOC_FREE(frame); + return LDB_ERR_NO_SUCH_OBJECT; + } + + match = get_best_match(dn, result); + if (match == NULL) { + TALLOC_FREE(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + + *msg = talloc_move(mem_ctx, &match); + TALLOC_FREE(frame); + return LDB_SUCCESS; +} + +/* + * Lookup a DNS record, will match DNS wild card records if an exact match + * is not found. + */ +WERROR dns_common_wildcard_lookup(struct ldb_context *samdb, + TALLOC_CTX *mem_ctx, + struct ldb_dn *dn, + struct dnsp_DnssrvRpcRecord **records, + uint16_t *num_records) +{ + int ret; + WERROR werr; + struct ldb_message *msg = NULL; + struct ldb_message_element *el = NULL; + const struct ldb_val *name = NULL; + + *records = NULL; + *num_records = 0; + + name = ldb_dn_get_rdn_val(dn); + if (name == NULL) { + return DNS_ERR(NAME_ERROR); + } + + werr = dns_name_check( + mem_ctx, + strlen((const char*)name->data), + (const char*) name->data); + if (!W_ERROR_IS_OK(werr)) { + return werr; + } + + ret = dns_wildcard_lookup(samdb, mem_ctx, dn, &msg); + if (ret == LDB_ERR_OPERATIONS_ERROR) { + return DNS_ERR(SERVER_FAILURE); + } + if (ret != LDB_SUCCESS) { + return DNS_ERR(NAME_ERROR); + } + + el = ldb_msg_find_element(msg, "dnsRecord"); + if (el == NULL) { + return WERR_DNS_ERROR_NAME_DOES_NOT_EXIST; + } + + werr = dns_common_extract(samdb, el, mem_ctx, records, num_records); + TALLOC_FREE(msg); + if (!W_ERROR_IS_OK(werr)) { + return werr; + } + + return WERR_OK; +} + static int rec_cmp(const struct dnsp_DnssrvRpcRecord *r1, const struct dnsp_DnssrvRpcRecord *r2) { diff --git a/source4/dns_server/dnsserver_common.h b/source4/dns_server/dnsserver_common.h index b615e2d..f2be44f 100644 --- a/source4/dns_server/dnsserver_common.h +++ b/source4/dns_server/dnsserver_common.h @@ -47,6 +47,11 @@ WERROR dns_common_lookup(struct ldb_context *samdb, struct dnsp_DnssrvRpcRecord **records, uint16_t *num_records, bool *tombstoned); +WERROR dns_common_wildcard_lookup(struct ldb_context *samdb, + TALLOC_CTX *mem_ctx, + struct ldb_dn *dn, + struct dnsp_DnssrvRpcRecord **records, + uint16_t *num_records); WERROR dns_name_check(TALLOC_CTX *mem_ctx, size_t len, const char *name); -- 1.9.1 From 30f57db8d321eb89740454782009b985175a4e70 Mon Sep 17 00:00:00 2001 From: Gary Lockyer Date: Mon, 7 Aug 2017 13:42:02 +1200 Subject: [PATCH 4/7] samba-tool dns: Test support of DNS wild card in names As DNS wild cards are now supported we need to allow '*' characters in the domain names. Signed-off-by: Gary Lockyer Reviewed-by: Andrew Bartlett Reviewed-by: Garming Sam BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952 (cherry picked from commit 3d2bd849f119480f0c1262c00e45179ceaa6e755) --- python/samba/tests/samba_tool/dnscmd.py | 67 +++++++++++++++++++++++++++++++++ selftest/knownfail.d/sambatooldns | 3 ++ 2 files changed, 70 insertions(+) create mode 100644 selftest/knownfail.d/sambatooldns diff --git a/python/samba/tests/samba_tool/dnscmd.py b/python/samba/tests/samba_tool/dnscmd.py index 3a369d9..1712c0e 100644 --- a/python/samba/tests/samba_tool/dnscmd.py +++ b/python/samba/tests/samba_tool/dnscmd.py @@ -659,3 +659,70 @@ class DnsCmdTestCase(SambaToolCmdTest): self.zone, "testrecord2", "A", self.testip, self.creds_string) self.assertCmdFail(result) + + def test_dns_wildcards(self): + """ + Ensure that DNS wild card entries can be added deleted and queried + """ + num_failures = 0 + failure_msgs = [] + records = [("*.", "MISS", "A", "1.1.1.1"), + ("*.SAMDOM", "MISS.SAMDOM", "A", "1.1.1.2")] + for (name, miss, dnstype, record) in records: + try: + result, out, err = self.runsubcmd("dns", "add", + os.environ["SERVER"], + self.zone, name, + dnstype, record, + self.creds_string) + self.assertCmdSuccess( + result, + out, + err, + ("Failed to add record %s (%s) with type %s." + % (name, record, dnstype))) + + result, out, err = self.runsubcmd("dns", "query", + os.environ["SERVER"], + self.zone, name, + dnstype, + self.creds_string) + self.assertCmdSuccess( + result, + out, + err, + ("Failed to query record %s with qualifier %s." + % (record, dnstype))) + + # dns tool does not perform dns wildcard search if the name + # does not match + result, out, err = self.runsubcmd("dns", "query", + os.environ["SERVER"], + self.zone, miss, + dnstype, + self.creds_string) + self.assertCmdFail( + result, + ("Failed to query record %s with qualifier %s." + % (record, dnstype))) + + result, out, err = self.runsubcmd("dns", "delete", + os.environ["SERVER"], + self.zone, name, + dnstype, record, + self.creds_string) + self.assertCmdSuccess( + result, + out, + err, + ("Failed to remove record %s with type %s." + % (record, dnstype))) + except AssertionError as e: + num_failures = num_failures + 1 + failure_msgs.append(e) + + if num_failures > 0: + for msg in failure_msgs: + print(msg) + self.fail("Failed to accept valid commands. %d total failures." + "Errors above." % num_failures) diff --git a/selftest/knownfail.d/sambatooldns b/selftest/knownfail.d/sambatooldns new file mode 100644 index 0000000..b60e9b2 --- /dev/null +++ b/selftest/knownfail.d/sambatooldns @@ -0,0 +1,3 @@ +# Support for DNS wildcared entries by samba_tool dns sub command +# Will fail until implemented. +^samba.tests.samba_tool.dnscmd.samba.tests.samba_tool.dnscmd.DnsCmdTestCase.test_dns_wildcards\(ad_dc:local\) -- 1.9.1 From d74731e2a06a20129ba490bf31409b2c320a4a23 Mon Sep 17 00:00:00 2001 From: Gary Lockyer Date: Thu, 20 Jul 2017 09:13:43 +1200 Subject: [PATCH 5/7] samba-tool dns query: Allow '*' in names As DNS wild cards are now supported we need to allow '*' characters in the domain names. Signed-off-by: Gary Lockyer Reviewed-by: Andrew Bartlett Reviewed-by: Garming Sam BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952 (cherry picked from commit 29b3a2b0d1e5abc1ca9f112f680251e34fa6e3af) --- python/samba/netcmd/dns.py | 3 ++- selftest/knownfail.d/sambatooldns | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 selftest/knownfail.d/sambatooldns diff --git a/python/samba/netcmd/dns.py b/python/samba/netcmd/dns.py index 6f88817..fd8db93 100644 --- a/python/samba/netcmd/dns.py +++ b/python/samba/netcmd/dns.py @@ -819,7 +819,8 @@ class cmd_query(Command): record_type = dns_type_flag(rtype) if name.find('*') != -1: - raise CommandError('Wildcard searches not supported. To dump entire zone use "@"') + self.outf.write('use "@" to dump entire domain, looking up %s\n' % + name) select_flags = 0 if authority: diff --git a/selftest/knownfail.d/sambatooldns b/selftest/knownfail.d/sambatooldns deleted file mode 100644 index b60e9b2..0000000 --- a/selftest/knownfail.d/sambatooldns +++ /dev/null @@ -1,3 +0,0 @@ -# Support for DNS wildcared entries by samba_tool dns sub command -# Will fail until implemented. -^samba.tests.samba_tool.dnscmd.samba.tests.samba_tool.dnscmd.DnsCmdTestCase.test_dns_wildcards\(ad_dc:local\) -- 1.9.1 From cea9e5733e79bce7472b2d6765d1fd6029ad4e76 Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Tue, 29 Aug 2017 11:48:46 +1200 Subject: [PATCH 6/7] s4-dnsserver: Always encode user-supplied names when looking up DNS records BUG: https://bugzilla.samba.org/show_bug.cgi?id=12994 Signed-off-by: Andrew Bartlett Reviewed-by: Garming Sam (cherry picked from commit c17470210792e6443bd3c28c18874645f1558494) --- source4/rpc_server/dnsserver/dcerpc_dnsserver.c | 15 ++++++++++++--- source4/rpc_server/dnsserver/dnsdb.c | 15 +++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/source4/rpc_server/dnsserver/dcerpc_dnsserver.c b/source4/rpc_server/dnsserver/dcerpc_dnsserver.c index 286da18..120d4b9 100644 --- a/source4/rpc_server/dnsserver/dcerpc_dnsserver.c +++ b/source4/rpc_server/dnsserver/dcerpc_dnsserver.c @@ -1674,10 +1674,13 @@ static WERROR dnsserver_enumerate_root_records(struct dnsserver_state *dsstate, /* Add any additional records */ if (select_flag & DNS_RPC_VIEW_ADDITIONAL_DATA) { for (i=0; isamdb, tmp_ctx, &res, z->zone_dn, LDB_SCOPE_ONELEVEL, attrs, "(&(objectClass=dnsNode)(name=%s)(!(dNSTombstoned=TRUE)))", - add_names[i]); + encoded_name); if (ret != LDB_SUCCESS || res->count == 0) { talloc_free(res); continue; @@ -1744,10 +1747,12 @@ static WERROR dnsserver_enumerate_records(struct dnsserver_state *dsstate, LDB_SCOPE_ONELEVEL, attrs, "(&(objectClass=dnsNode)(!(dNSTombstoned=TRUE)))"); } else { + char *encoded_name + = ldb_binary_encode_string(tmp_ctx, name); ret = ldb_search(dsstate->samdb, tmp_ctx, &res, z->zone_dn, LDB_SCOPE_ONELEVEL, attrs, "(&(objectClass=dnsNode)(|(name=%s)(name=*.%s))(!(dNSTombstoned=TRUE)))", - name, name); + encoded_name, encoded_name); } if (ret != LDB_SUCCESS) { talloc_free(tmp_ctx); @@ -1818,11 +1823,15 @@ static WERROR dnsserver_enumerate_records(struct dnsserver_state *dsstate, /* Search all the available zones for additional name */ for (z2 = dsstate->zones; z2; z2 = z2->next) { + char *encoded_name; name = dns_split_node_name(tmp_ctx, add_names[i], z2->name); + encoded_name + = ldb_binary_encode_string(tmp_ctx, + name); ret = ldb_search(dsstate->samdb, tmp_ctx, &res, z2->zone_dn, LDB_SCOPE_ONELEVEL, attrs, "(&(objectClass=dnsNode)(name=%s)(!(dNSTombstoned=TRUE)))", - name); + encoded_name); talloc_free(name); if (ret != LDB_SUCCESS) { continue; diff --git a/source4/rpc_server/dnsserver/dnsdb.c b/source4/rpc_server/dnsserver/dnsdb.c index da37878..15fe565 100644 --- a/source4/rpc_server/dnsserver/dnsdb.c +++ b/source4/rpc_server/dnsserver/dnsdb.c @@ -364,10 +364,12 @@ WERROR dnsserver_db_add_empty_node(TALLOC_CTX *mem_ctx, const char * const attrs[] = { "name", NULL }; struct ldb_result *res; struct ldb_dn *dn; + char *encoded_name = ldb_binary_encode_string(mem_ctx, name); int ret; ret = ldb_search(samdb, mem_ctx, &res, z->zone_dn, LDB_SCOPE_BASE, attrs, - "(&(objectClass=dnsNode)(name=%s))", name); + "(&(objectClass=dnsNode)(name=%s))", + encoded_name); if (ret != LDB_SUCCESS) { return WERR_INTERNAL_DB_ERROR; } @@ -406,6 +408,7 @@ WERROR dnsserver_db_add_record(TALLOC_CTX *mem_ctx, int serial; WERROR werr; bool was_tombstoned = false; + char *encoded_name = ldb_binary_encode_string(mem_ctx, name); werr = dns_to_dnsp_convert(mem_ctx, add_record, &rec, true); if (!W_ERROR_IS_OK(werr)) { @@ -436,7 +439,8 @@ WERROR dnsserver_db_add_record(TALLOC_CTX *mem_ctx, rec->dwTimeStamp = t; ret = ldb_search(samdb, mem_ctx, &res, z->zone_dn, LDB_SCOPE_ONELEVEL, attrs, - "(&(objectClass=dnsNode)(name=%s))", name); + "(&(objectClass=dnsNode)(name=%s))", + encoded_name); if (ret != LDB_SUCCESS) { return WERR_INTERNAL_DB_ERROR; } @@ -524,6 +528,7 @@ WERROR dnsserver_db_update_record(TALLOC_CTX *mem_ctx, int ret, i; int serial; WERROR werr; + char *encoded_name = ldb_binary_encode_string(mem_ctx, name); werr = dns_to_dnsp_convert(mem_ctx, add_record, &arec, true); if (!W_ERROR_IS_OK(werr)) { @@ -541,7 +546,8 @@ WERROR dnsserver_db_update_record(TALLOC_CTX *mem_ctx, arec->dwTimeStamp = t; ret = ldb_search(samdb, mem_ctx, &res, z->zone_dn, LDB_SCOPE_ONELEVEL, attrs, - "(&(objectClass=dnsNode)(name=%s)(!(dNSTombstoned=TRUE)))", name); + "(&(objectClass=dnsNode)(name=%s)(!(dNSTombstoned=TRUE)))", + encoded_name); if (ret != LDB_SUCCESS) { return WERR_INTERNAL_DB_ERROR; } @@ -642,7 +648,8 @@ WERROR dnsserver_db_delete_record(TALLOC_CTX *mem_ctx, } ret = ldb_search(samdb, mem_ctx, &res, z->zone_dn, LDB_SCOPE_ONELEVEL, attrs, - "(&(objectClass=dnsNode)(name=%s))", name); + "(&(objectClass=dnsNode)(name=%s))", + ldb_binary_encode_string(mem_ctx, name)); if (ret != LDB_SUCCESS) { return WERR_INTERNAL_DB_ERROR; } -- 1.9.1 From 9eb8e8ce5dbe5badcbdd57cc1040a43124531bb1 Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Tue, 29 Aug 2017 14:19:22 +1200 Subject: [PATCH 7/7] s4-dnsserver: Check for too many DNS results If we had this check in when the wildcard DNS tests were written, we would have noticed that the name needed to be escaped (see previous commit). BUG: https://bugzilla.samba.org/show_bug.cgi?id=12994 Signed-off-by: Andrew Bartlett Reviewed-by: Douglas Bagnall Reviewed-by: Garming Sam (cherry picked from commit 9e9a8d8f887a3b13d06a7cc71edad78c140bb0be) --- source4/rpc_server/dnsserver/dnsdb.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source4/rpc_server/dnsserver/dnsdb.c b/source4/rpc_server/dnsserver/dnsdb.c index 15fe565..81a2d20 100644 --- a/source4/rpc_server/dnsserver/dnsdb.c +++ b/source4/rpc_server/dnsserver/dnsdb.c @@ -657,6 +657,9 @@ WERROR dnsserver_db_delete_record(TALLOC_CTX *mem_ctx, if (res->count == 0) { return WERR_DNS_ERROR_RECORD_DOES_NOT_EXIST; } + if (res->count > 1) { + return WERR_DNS_ERROR_RCODE_SERVER_FAILURE; + } el = ldb_msg_find_element(res->msgs[0], "dnsRecord"); if (el == NULL || el->num_values == 0) { -- 1.9.1