[PATCH] Add DNS widkcard support.

Andrew Bartlett abartlet at samba.org
Wed Aug 9 04:12:31 UTC 2017


On Fri, 2017-08-04 at 07:19 +1200, Gary Lockyer via samba-technical
wrote:
> Updated patch set incorporating Andrews and Volkers feedback.
> 
> Reviews appreciated.

Reviewed-by: Andrew Bartlett <abartlet at samba.org>
Reviewed-by: Garming Sam <garming at catalyst.net.nz>

BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952

I've pushed the attached to autobuild.

Thanks!

Andrew Bartlett
-- 
Andrew Bartlett
https://samba.org/~abartlet/
Authentication Developer, Samba Team         https://samba.org
Samba Development and Support, Catalyst IT   
https://catalyst.net.nz/services/samba



-------------- next part --------------
From 047cf17ddb39b04a84ff8e9b21963f14c866fab2 Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Tue, 25 Jul 2017 14:14:53 +1200
Subject: [PATCH 1/5] dnsserver: Tests for dns wildcard entries

Add tests for dns wildcards.
Tests validated against Windows Server 2012 R2

Signed-off-by: Gary Lockyer <gary at catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet at samba.org>
Reviewed-by: Garming Sam <garming at catalyst.net.nz>
BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952
---
 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 00000000000..ca8426a6f14
--- /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 <http://www.gnu.org/licenses/>.
+#
+
+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 <server name> <server ip> [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 00000000000..7e7892f3ee4
--- /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 3f3d21685ff..da48730bb96 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')
 
-- 
2.11.0


From 8497334913134275aafa3d32daee1cd9664dc6df Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Thu, 3 Aug 2017 15:12:51 +1200
Subject: [PATCH 2/5] 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 <gary at catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet at samba.org>
Reviewed-by: Garming Sam <garming at catalyst.net.nz>
BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952
---
 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 aebb106b053..8e8eed5ab23 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 a56ff08031b..2a81b836722 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;
-- 
2.11.0


From 3dd22ecad9d1d22e732425c6ca65d2adbf49682c Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Thu, 3 Aug 2017 15:12:02 +1200
Subject: [PATCH 3/5] 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 <gary at catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet at samba.org>
Reviewed-by: Garming Sam <garming at catalyst.net.nz>
BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952
---
 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 | 342 ++++++++++++++++++++++++++++++++++
 source4/dns_server/dnsserver_common.h |   5 +
 7 files changed, 374 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 7e7892f3ee4..00000000000
--- 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 7096f4749b2..6ef378c75a6 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 4b5bb0772a4..fa9272156ab 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 5395ff95161..382b6bdf95b 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 c728eaa8d39..ee35bd223f7 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 2a81b836722..1b1c87040dc 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,344 @@ 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  */
+	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) {
+		ret = ldb_wait(request->handle, LDB_WAIT_ALL);
+	}
+	TALLOC_FREE(request);
+
+	if (ret == LDB_SUCCESS) {
+		if (result->count == 0) {
+			ret = LDB_ERR_NO_SUCH_OBJECT;
+		} else {
+			struct ldb_message *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 ret;
+}
+
+/*
+ * 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 b615e2dcfae..f2be44ff0d6 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);
-- 
2.11.0


From 367808ea447ce8ba0d88ae05c5cd081669e6c7d3 Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Mon, 7 Aug 2017 13:42:02 +1200
Subject: [PATCH 4/5] 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 <gary at catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet at samba.org>
Reviewed-by: Garming Sam <garming at catalyst.net.nz>
BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952
---
 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 3a369d95b8a..1712c0efde1 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 00000000000..b60e9b21e73
--- /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\)
-- 
2.11.0


From 8a33f7ec8e147ace387b7a28442404828a1b1e70 Mon Sep 17 00:00:00 2001
From: Gary Lockyer <gary at catalyst.net.nz>
Date: Thu, 20 Jul 2017 09:13:43 +1200
Subject: [PATCH 5/5] 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 <gary at catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet at samba.org>
Reviewed-by: Garming Sam <garming at catalyst.net.nz>
BUG: https://bugzilla.samba.org/show_bug.cgi?id=12952
---
 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 6f88817d701..fd8db937a52 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 b60e9b21e73..00000000000
--- 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\)
-- 
2.11.0



More information about the samba-technical mailing list