join.py Add DNS records at domain join time
authorAndrew Bartlett <abartlet@samba.org>
Fri, 17 Feb 2017 05:23:23 +0000 (18:23 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Sun, 11 Jun 2017 00:04:51 +0000 (02:04 +0200)
This avoids issues getting replication going after the DC first starts
as the rest of the domain does not have to wait for samba_dnsupdate to
run successfully

We do not just run samba_dnsupdate as we want to strictly
operate against the DC we just joined:
 - We do not want to query another DNS server
 - We do not want to obtain a Kerberos ticket for the new DC
   (as the KDC we select may not be the DC we just joined,
   and so may not be in sync with the password we just set)
 - We do not wish to set the _ldap records until we have started
 - We do not wish to use NTLM (the --use-samba-tool mode forces
   NTLM)

The downside to using DCE/RPC rather than DNS is that these will
be regarded as static entries, and (against windows) have a the ACL
assigned for static entries.  However this is still better than no
DNS at all.

Because some tests want a DNS record matching their own name
this fixes some tests and removes entires from knownfail

Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
Autobuild-User(master): Andrew Bartlett <abartlet@samba.org>
Autobuild-Date(master): Sun Jun 11 02:04:52 CEST 2017 on sn-devel-144

python/samba/join.py
selftest/knownfail.d/dns
selftest/knownfail.d/dns-at-join [deleted file]

index a76772a5b0f4fa09c4bccfb681da09dbaa554374..fa87f0bb32f316d55f773f6e20faebe099fd683d 100644 (file)
@@ -22,8 +22,8 @@ from samba.auth import system_session
 from samba.samdb import SamDB
 from samba import gensec, Ldb, drs_utils, arcfour_encrypt, string_to_byte_array
 import ldb, samba, sys, uuid
-from samba.ndr import ndr_pack
-from samba.dcerpc import security, drsuapi, misc, nbt, lsa, drsblobs
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import security, drsuapi, misc, nbt, lsa, drsblobs, dnsserver, dnsp
 from samba.dsdb import DS_DOMAIN_FUNCTION_2003
 from samba.credentials import Credentials, DONT_USE_KERBEROS
 from samba.provision import secretsdb_self_join, provision, provision_fill, FILL_DRS, FILL_SUBDOMAIN
@@ -35,6 +35,9 @@ from samba.provision.sambadns import setup_bind9_dns
 from samba import read_and_sub_file
 from samba import werror
 from base64 import b64encode
+from samba import WERRORError
+from samba.dnsserver import ARecord, AAAARecord, PTRRecord, CNameRecord, NSRecord, MXRecord, SOARecord, SRVRecord, TXTRecord
+from samba import sd_utils
 import logging
 import talloc
 import random
@@ -187,6 +190,12 @@ class dc_join(object):
         ctx.adminpass = None
         ctx.partition_dn = None
 
+        ctx.dns_a_dn = None
+        ctx.dns_cname_dn = None
+
+        # Do not normally register 127. addresses but allow override for selftest
+        ctx.force_all_ips = False
+
     def del_noerror(ctx, dn, recursive=False):
         if recursive:
             try:
@@ -294,6 +303,13 @@ class dc_join(object):
 
             lsaconn.DeleteTrustedDomain(pol_handle, info.info_ex.sid)
 
+        if ctx.dns_a_dn:
+            ctx.del_noerror(ctx.dns_a_dn)
+
+        if ctx.dns_cname_dn:
+            ctx.del_noerror(ctx.dns_cname_dn)
+
+
 
     def promote_possible(ctx):
         """confirm that the account is just a bare NT4 BDC or a member server, so can be safely promoted"""
@@ -692,12 +708,16 @@ class dc_join(object):
                                      newpassword=ctx.acct_pass.encode('utf-8'))
 
             res = ctx.samdb.search(base=ctx.acct_dn, scope=ldb.SCOPE_BASE,
-                                   attrs=["msDS-KeyVersionNumber"])
+                                   attrs=["msDS-KeyVersionNumber",
+                                          "objectSID"])
             if "msDS-KeyVersionNumber" in res[0]:
                 ctx.key_version_number = int(res[0]["msDS-KeyVersionNumber"][0])
             else:
                 ctx.key_version_number = None
 
+            ctx.new_dc_account_sid = ndr_unpack(security.dom_sid,
+                                                res[0]["objectSid"][0])
+
             print("Enabling account")
             m = ldb.Message()
             m.dn = ldb.Dn(ctx.samdb, ctx.acct_dn)
@@ -974,6 +994,175 @@ class dc_join(object):
 
         ctx.drsuapi.DsReplicaUpdateRefs(ctx.drsuapi_handle, 1, r)
 
+    def join_add_dns_records(ctx):
+        """Remotely Add a DNS record to the target DC.  We assume that if we
+           replicate DNS that the server holds the DNS roles and can accept
+           updates.
+
+           This avoids issues getting replication going after the DC
+           first starts as the rest of the domain does not have to
+           wait for samba_dnsupdate to run successfully.
+
+           Specifically, we add the records implied by the DsReplicaUpdateRefs
+           call above.
+
+           We do not just run samba_dnsupdate as we want to strictly
+           operate against the DC we just joined:
+            - We do not want to query another DNS server
+            - We do not want to obtain a Kerberos ticket
+              (as the KDC we select may not be the DC we just joined,
+              and so may not be in sync with the password we just set)
+            - We do not wish to set the _ldap records until we have started
+            - We do not wish to use NTLM (the --use-samba-tool mode forces
+              NTLM)
+
+        """
+
+        client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
+        record_type = dnsp.DNS_TYPE_A
+        select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA |\
+                       dnsserver.DNS_RPC_VIEW_NO_CHILDREN
+
+        zone = ctx.dnsdomain
+        msdcs_zone = "_msdcs.%s" % ctx.dnsforest
+        name = ctx.myname
+        msdcs_cname = str(ctx.ntds_guid)
+        cname_target = "%s.%s" % (name, zone)
+        IPs = samba.interface_ips(ctx.lp, ctx.force_all_ips)
+
+        ctx.logger.info("Adding %d remote DNS records for %s.%s" % \
+                        (len(IPs), name, zone))
+
+        binding_options = "sign"
+        dns_conn = dnsserver.dnsserver("ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options),
+                                      ctx.lp, ctx.creds)
+
+
+        name_found = True
+
+        sd_helper = samba.sd_utils.SDUtils(ctx.samdb)
+
+        change_owner_sd = security.descriptor()
+        change_owner_sd.owner_sid = ctx.new_dc_account_sid
+        change_owner_sd.group_sid = security.dom_sid("%s-%d" %
+                                                     (str(ctx.domsid),
+                                                      security.DOMAIN_RID_DCS))
+
+        # TODO: Remove any old records from the primary DNS name
+        try:
+            (buflen, res) \
+                = dns_conn.DnssrvEnumRecords2(client_version,
+                                              0,
+                                              ctx.server,
+                                              zone,
+                                              name,
+                                              None,
+                                              dnsp.DNS_TYPE_ALL,
+                                              select_flags,
+                                              None,
+                                              None)
+        except WERRORError as e:
+            if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
+                name_found = False
+                pass
+
+        if name_found:
+            for rec in res.rec:
+                for record in rec.records:
+                    if record.wType == dnsp.DNS_TYPE_A or \
+                       record.wType == dnsp.DNS_TYPE_AAAA:
+                        # delete record
+                        del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
+                        del_rec_buf.rec = record
+                        try:
+                            dns_conn.DnssrvUpdateRecord2(client_version,
+                                                         0,
+                                                         ctx.server,
+                                                         zone,
+                                                         name,
+                                                         None,
+                                                         del_rec_buf)
+                        except WERRORError as e:
+                            if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
+                                pass
+                            else:
+                                raise
+
+        for IP in IPs:
+            if IP.find(':') != -1:
+                ctx.logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s"
+                                % (name, zone, IP))
+                rec = AAAARecord(IP)
+            else:
+                ctx.logger.info("Adding DNS A record %s.%s for IPv4 IP: %s"
+                                % (name, zone, IP))
+                rec = ARecord(IP)
+
+            # Add record
+            add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
+            add_rec_buf.rec = rec
+            dns_conn.DnssrvUpdateRecord2(client_version,
+                                         0,
+                                         ctx.server,
+                                         zone,
+                                         name,
+                                         add_rec_buf,
+                                         None)
+
+        if (len(IPs) > 0):
+            domaindns_zone_dn = ldb.Dn(ctx.samdb, ctx.domaindns_zone)
+            (ctx.dns_a_dn, ldap_record) \
+                = ctx.samdb.dns_lookup("%s.%s" % (name, zone),
+                                       dns_partition=domaindns_zone_dn)
+
+            # Make the DC own the DNS record, not the administrator
+            sd_helper.modify_sd_on_dn(ctx.dns_a_dn, change_owner_sd,
+                                      controls=["sd_flags:1:%d"
+                                                % (security.SECINFO_OWNER
+                                                   | security.SECINFO_GROUP)])
+
+
+            # Add record
+            ctx.logger.info("Adding DNS CNAME record %s.%s for %s"
+                            % (msdcs_cname, msdcs_zone, cname_target))
+
+            add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
+            rec = CNameRecord(cname_target)
+            add_rec_buf.rec = rec
+            dns_conn.DnssrvUpdateRecord2(client_version,
+                                         0,
+                                         ctx.server,
+                                         msdcs_zone,
+                                         msdcs_cname,
+                                         add_rec_buf,
+                                         None)
+
+            forestdns_zone_dn = ldb.Dn(ctx.samdb, ctx.forestdns_zone)
+            (ctx.dns_cname_dn, ldap_record) \
+                = ctx.samdb.dns_lookup("%s.%s" % (msdcs_cname, msdcs_zone),
+                                       dns_partition=forestdns_zone_dn)
+
+            # Make the DC own the DNS record, not the administrator
+            sd_helper.modify_sd_on_dn(ctx.dns_cname_dn, change_owner_sd,
+                                      controls=["sd_flags:1:%d"
+                                                % (security.SECINFO_OWNER
+                                                   | security.SECINFO_GROUP)])
+
+        ctx.logger.info("All other DNS records (like _ldap SRV records) " +
+                        "will be created samba_dnsupdate on first startup")
+
+
+    def join_replicate_new_dns_records(ctx):
+        for nc in (ctx.domaindns_zone, ctx.forestdns_zone):
+            if nc in ctx.nc_list:
+                ctx.logger.info("Replicating new DNS records in %s" % (str(nc)))
+                ctx.repl.replicate(nc, ctx.source_dsa_invocation_id,
+                                   ctx.ntds_guid, rodc=ctx.RODC,
+                                   replica_flags=ctx.replica_flags,
+                                   full_sync=False)
+
+
+
     def join_finalise(ctx):
         """Finalise the join, mark us synchronised and setup secrets db."""
 
@@ -1190,6 +1379,11 @@ class dc_join(object):
                 ctx.join_add_objects2()
                 ctx.join_provision_own_domain()
                 ctx.join_setup_trusts()
+
+            if not ctx.clone_only and ctx.dns_backend != "NONE":
+                ctx.join_add_dns_records()
+                ctx.join_replicate_new_dns_records()
+
             ctx.join_finalise()
         except:
             try:
index c40041d18924b60d4a8f34b10330ae613620b15a..cb3003240ea30e3a89c1e8aab7c21e730d4ea13e 100644 (file)
@@ -36,19 +36,12 @@ samba.tests.dns.__main__.TestRPCRoundtrip.test_update_add_txt_rpc_to_dns\(rodc:l
 samba.tests.dns.__main__.TestZones.test_soa_query\(rodc:local\)
 samba.tests.dns.__main__.TestComplexQueries.test_cname_two_chain\(vampire_dc:local\)
 samba.tests.dns.__main__.TestComplexQueries.test_one_a_query\(vampire_dc:local\)
-
-# The SOA override should not pass against the RODC, it must not overstamp
-samba.tests.dns.__main__.TestSimpleQueries.test_one_SOA_query\(rodc:local\)
-
-# The very first DC will have DNS records, but subsequent DCs only get entries into
-# the dns_hosts_file in our selftest env
 samba.tests.dns.__main__.TestSimpleQueries.test_one_a_query\(vampire_dc:local\)
 samba.tests.dns.__main__.TestSimpleQueries.test_one_a_query_tcp\(vampire_dc:local\)
-samba.tests.dns.__main__.TestSimpleQueries.test_one_mx_query\(vampire_dc:local\)
 samba.tests.dns.__main__.TestSimpleQueries.test_qtype_all_query\(vampire_dc:local\)
-samba.tests.dns.__main__.TestSimpleQueries.test_soa_hostname_query\(vampire_dc:local\)
 samba.tests.dns.__main__.TestSimpleQueries.test_one_a_query\(rodc:local\)
 samba.tests.dns.__main__.TestSimpleQueries.test_one_a_query_tcp\(rodc:local\)
-samba.tests.dns.__main__.TestSimpleQueries.test_one_mx_query\(rodc:local\)
 samba.tests.dns.__main__.TestSimpleQueries.test_qtype_all_query\(rodc:local\)
-samba.tests.dns.__main__.TestSimpleQueries.test_soa_hostname_query\(rodc:local\)
+
+# The SOA override should not pass against the RODC, it must not overstamp
+samba.tests.dns.__main__.TestSimpleQueries.test_one_SOA_query\(rodc:local\)
diff --git a/selftest/knownfail.d/dns-at-join b/selftest/knownfail.d/dns-at-join
deleted file mode 100644 (file)
index 3737f1f..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-samba.tests.join.python\(ad_dc_ntvfs\).samba.tests.join.JoinTestCase.test_join_makes_records\(ad_dc_ntvfs\)
-samba.tests.join.python\(ad_dc_ntvfs\).samba.tests.join.JoinTestCase.test_join_records_can_update\(ad_dc_ntvfs\)