from socket import AF_INET
from socket import AF_INET6
import struct
+import time
+import ldb
+from samba.ndr import ndr_unpack, ndr_pack
+import re
-from samba import remove_dc
+from samba import remove_dc, dsdb_dns
from samba.samdb import SamDB
from samba.auth import system_session
Option('--client-version', help='Client Version',
default='longhorn', metavar='w2k|dotnet|longhorn',
choices=['w2k', 'dotnet', 'longhorn'], dest='cli_ver'),
+ Option('--mark-old-records-static',
+ help="Make records older than this (YYYY-MM-DD) static"),
+ Option('--mark-records-static-regex', metavar="REGEXP",
+ help="Make records matching this regular expression static"),
+ Option('--mark-records-dynamic-regex', metavar="REGEXP",
+ help="Make records matching this regular expression dynamic"),
+ Option('-n', '--dry-run', action='store_true',
+ help="Don't change anything, say what would happen"),
]
integer_properties = []
integer_properties)
def run(self, server, zone, cli_ver, sambaopts=None, credopts=None,
- versionopts=None, **kwargs):
+ versionopts=None, dry_run=False,
+ mark_old_records_static=None,
+ mark_records_static_regex=None,
+ mark_records_dynamic_regex=None,
+ **kwargs):
self.lp = sambaopts.get_loadparm()
self.creds = credopts.get_credentials(self.lp)
dns_conn = dns_connect(server, self.lp, self.creds)
name_param = dnsserver.DNS_RPC_NAME_AND_PARAM()
name_param.dwParam = v
name_param.pszNodeName = k
+ if dry_run:
+ print(f"would set {k} to {v} for {zone}", file=self.outf)
+ continue
try:
dns_conn.DnssrvOperation2(client_version,
0,
print(f"Set {k} to {v}", file=self.outf)
+ # We don't want to allow more than one of these --mark-*
+ # options at a time, as they are sensitive to ordering and
+ # the order is not documented.
+ n_mark_options = 0
+ for x in (mark_old_records_static,
+ mark_records_static_regex,
+ mark_records_dynamic_regex):
+ if x is not None:
+ n_mark_options += 1
+
+ if n_mark_options > 1:
+ raise CommandError("Multiple --mark-* options will not work\n")
+
+ if mark_old_records_static is not None:
+ self.mark_old_records_static(server, zone,
+ mark_old_records_static,
+ dry_run)
+
+ if mark_records_static_regex is not None:
+ self.mark_records_static_regex(server,
+ zone,
+ mark_records_static_regex,
+ dry_run)
+
+ if mark_records_dynamic_regex is not None:
+ self.mark_records_dynamic_regex(server,
+ zone,
+ mark_records_dynamic_regex,
+ dry_run)
+
+
+ def _get_dns_nodes(self, server, zone_name):
+ samdb = SamDB(url="ldap://%s" % server,
+ session_info=system_session(),
+ credentials=self.creds, lp=self.lp)
+
+ zone_dn = (f"DC={zone_name},CN=MicrosoftDNS,DC=DomainDNSZones,"
+ f"{samdb.get_default_basedn()}")
+
+ nodes = samdb.search(base=zone_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ expression=("(&(objectClass=dnsNode)"
+ "(!(dNSTombstoned=TRUE)))"),
+ attrs=["dnsRecord", "name"])
+ return samdb, nodes
+
+ def mark_old_records_static(self, server, zone_name, date_string, dry_run):
+ try:
+ ts = time.strptime(date_string, "%Y-%m-%d")
+ t = time.mktime(ts)
+ except ValueError as e:
+ raise CommandError(f"Invalid date {date_string}: should be YYY-MM-DD")
+ threshold = dsdb_dns.unix_to_dns_timestamp(int(t))
+
+ samdb, nodes = self._get_dns_nodes(server, zone_name)
+
+ for node in nodes:
+ if "dnsRecord" not in node:
+ continue
+
+ values = list(node["dnsRecord"])
+ changes = 0
+ for i, v in enumerate(values):
+ rec = ndr_unpack(dnsp.DnssrvRpcRecord, v)
+ if rec.dwTimeStamp < threshold and rec.dwTimeStamp != 0:
+ rec.dwTimeStamp = 0
+ values[i] = ndr_pack(rec)
+ changes += 1
+
+ if changes == 0:
+ continue
+
+ name = node["name"][0].decode()
+
+ if dry_run:
+ print(f"would make {changes}/{len(values)} records static "
+ f"on {name}.{zone_name}.", file=self.outf)
+ continue
+
+ msg = ldb.Message.from_dict(samdb,
+ {'dn': node.dn,
+ 'dnsRecord': values
+ },
+ ldb.FLAG_MOD_REPLACE)
+ samdb.modify(msg)
+ print(f"made {changes}/{len(values)} records static on "
+ f"{name}.{zone_name}.", file=self.outf)
+
+ def mark_records_static_regex(self, server, zone_name, regex, dry_run):
+ """Make the records of nodes with matching names static.
+ """
+ r = re.compile(regex)
+ samdb, nodes = self._get_dns_nodes(server, zone_name)
+
+ for node in nodes:
+ name = node["name"][0].decode()
+ if not r.search(name):
+ continue
+ if "dnsRecord" not in node:
+ continue
+
+ values = list(node["dnsRecord"])
+ if len(values) == 0:
+ continue
+
+ changes = 0
+ for i, v in enumerate(values):
+ rec = ndr_unpack(dnsp.DnssrvRpcRecord, v)
+ if rec.dwTimeStamp != 0:
+ rec.dwTimeStamp = 0
+ values[i] = ndr_pack(rec)
+ changes += 1
+
+ if changes == 0:
+ continue
+
+ if dry_run:
+ print(f"would make {changes}/{len(values)} records static "
+ f"on {name}.{zone_name}.", file=self.outf)
+ continue
+
+ msg = ldb.Message.from_dict(samdb,
+ {'dn': node.dn,
+ 'dnsRecord': values
+ },
+ ldb.FLAG_MOD_REPLACE)
+ samdb.modify(msg)
+ print(f"made {changes}/{len(values)} records static on "
+ f"{name}.{zone_name}.", file=self.outf)
+
+ def mark_records_dynamic_regex(self, server, zone_name, regex, dry_run):
+ """Make the records of nodes with matching names dynamic, with a
+ current timestamp. In this case we only adjust the A, AAAA,
+ and TXT records.
+ """
+ r = re.compile(regex)
+ samdb, nodes = self._get_dns_nodes(server, zone_name)
+ now = time.time()
+ dns_timestamp = dsdb_dns.unix_to_dns_timestamp(int(now))
+ safe_wtypes = {
+ dnsp.DNS_TYPE_A,
+ dnsp.DNS_TYPE_AAAA,
+ dnsp.DNS_TYPE_TXT
+ }
+
+ for node in nodes:
+ name = node["name"][0].decode()
+ if not r.search(name):
+ continue
+ if "dnsRecord" not in node:
+ continue
+
+ values = list(node["dnsRecord"])
+ if len(values) == 0:
+ continue
+
+ changes = 0
+ for i, v in enumerate(values):
+ rec = ndr_unpack(dnsp.DnssrvRpcRecord, v)
+ if rec.wType in safe_wtypes and rec.dwTimeStamp == 0:
+ rec.dwTimeStamp = dns_timestamp
+ values[i] = ndr_pack(rec)
+ changes += 1
+
+ if changes == 0:
+ continue
+
+ if dry_run:
+ print(f"would make {changes}/{len(values)} records dynamic "
+ f"on {name}.{zone_name}.", file=self.outf)
+ continue
+
+ msg = ldb.Message.from_dict(samdb,
+ {'dn': node.dn,
+ 'dnsRecord': values
+ },
+ ldb.FLAG_MOD_REPLACE)
+ samdb.modify(msg)
+ print(f"made {changes}/{len(values)} records dynamic on "
+ f"{name}.{zone_name}.", file=self.outf)
+
class cmd_zoneinfo(Command):
"""Query for zone information."""
from samba.ndr import ndr_unpack, ndr_pack
from samba.dcerpc import dnsp
from samba.tests.samba_tool.base import SambaToolCmdTest
+import time
+from samba import dsdb_dns
class DnsCmdTestCase(SambaToolCmdTest):
self.creds_string)
self.assertCmdSuccess(result, out, err)
+ def get_all_records(self, zone_name):
+ zone_dn = (f"DC={zone_name},CN=MicrosoftDNS,DC=DomainDNSZones,"
+ f"{self.samdb.get_default_basedn()}")
+
+ expression = "(&(objectClass=dnsNode)(!(dNSTombstoned=TRUE)))"
+
+ nodes = self.samdb.search(base=zone_dn, scope=ldb.SCOPE_SUBTREE,
+ expression=expression,
+ attrs=["dnsRecord", "name"])
+
+ record_map = {}
+ for node in nodes:
+ name = node["name"][0].decode()
+ record_map[name] = list(node["dnsRecord"])
+
+ return record_map
+
def get_record_from_db(self, zone_name, record_name):
zones = self.samdb.search(base="DC=DomainDnsZones,%s"
% self.samdb.get_default_basedn(),
"Failed to print zoneinfo")
self.assertTrue(out != '')
- def test_zoneoptions(self):
+ def test_zoneoptions_aging(self):
for options, vals, error in (
(['--aging=1'], {'fAging': 'TRUE'}, False),
(['--aging=0'], {'fAging': 'FALSE'}, False),
for k, v in vals.items():
self.assertIn(k, info)
self.assertEqual(v, info[k])
+
+
+ def ldap_add_node_with_records(self, name, records):
+ dn = (f"DC={name},DC={self.zone},CN=MicrosoftDNS,DC=DomainDNSZones,"
+ f"{self.samdb.get_default_basedn()}")
+
+ dns_records = []
+ for r in records:
+ rec = dnsp.DnssrvRpcRecord()
+ rec.wType = r.get('wType', dnsp.DNS_TYPE_A)
+ rec.rank = dnsp.DNS_RANK_ZONE
+ rec.dwTtlSeconds = 900
+ rec.dwTimeStamp = r.get('dwTimeStamp', 0)
+ rec.data = r.get('data', '10.10.10.10')
+ dns_records.append(ndr_pack(rec))
+
+ msg = ldb.Message.from_dict(self.samdb,
+ {'dn': dn,
+ "objectClass": ["top", "dnsNode"],
+ 'dnsRecord': dns_records
+ })
+ self.samdb.add(msg)
+
+ def get_timestamp_map(self):
+ re_wtypes = (dnsp.DNS_TYPE_A,
+ dnsp.DNS_TYPE_AAAA,
+ dnsp.DNS_TYPE_TXT)
+
+ t = time.time()
+ now = dsdb_dns.unix_to_dns_timestamp(int(t))
+
+ records = self.get_all_records(self.zone)
+ tsmap = {}
+ for k, recs in records.items():
+ m = []
+ tsmap[k] = m
+ for rec in recs:
+ r = ndr_unpack(dnsp.DnssrvRpcRecord, rec)
+ timestamp = r.dwTimeStamp
+ if abs(timestamp - now) < 3:
+ timestamp = 'nowish'
+
+ if r.wType in re_wtypes:
+ m.append(('R', timestamp))
+ else:
+ m.append(('-', timestamp))
+
+ return tsmap
+
+
+ def test_zoneoptions_mark_records(self):
+ self.maxDiff = 10000
+ # We need a number of records to work with, so we'll use part
+ # of our known good records list, using three different names
+ # to test the regex. All these records will be static.
+ for dnstype in self.good_records:
+ for record in self.good_records[dnstype][:2]:
+ self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "frobitz",
+ dnstype, record,
+ self.creds_string)
+ self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "weergly",
+ dnstype, record,
+ self.creds_string)
+ self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "snizle",
+ dnstype, record,
+ self.creds_string)
+
+ # and we also want some that aren't static, and some mixed
+ # static/dynamic records.
+ # timestamps are in hours since 1601; now ~= 3.7 million
+ for ts in (0, 100, 10 ** 6, 10 ** 7):
+ name = f"ts-{ts}"
+ self.ldap_add_node_with_records(name, [{"dwTimeStamp": ts}])
+
+ recs = []
+ for ts in (0, 100, 10 ** 6, 10 ** 7):
+ addr = f'10.{(ts >> 16) & 255}.{(ts >> 8) & 255}.{ts & 255}'
+ recs.append({"dwTimeStamp": ts, "data": addr})
+
+ self.ldap_add_node_with_records("ts-multi", recs)
+
+ # get the state of ALL records.
+ # then we make assertions about the diffs, keeping track of
+ # the current state.
+
+ tsmap = self.get_timestamp_map()
+
+
+
+ for options, diff, output_substrings, error in (
+ # --mark-old-records-static
+ # --mark-records-static-regex
+ # --mark-records-dynamic-regex
+ (
+ ['--mark-old-records-static=1971-13-04'],
+ {},
+ [],
+ "bad date"
+ ),
+ (
+ # using --dry-run, should be no change, but output.
+ ['--mark-old-records-static=1971-03-04', '--dry-run'],
+ {},
+ [
+ "would make 1/1 records static on ts-1000000.zone.",
+ "would make 1/1 records static on ts-100.zone.",
+ "would make 2/4 records static on ts-multi.zone.",
+ ],
+ False
+ ),
+ (
+ # timestamps < ~ 3.25 million are now static
+ ['--mark-old-records-static=1971-03-04'],
+ {
+ 'ts-100': [('R', 0)],
+ 'ts-1000000': [('R', 0)],
+ 'ts-multi': [('R', 0), ('R', 0), ('R', 0), ('R', 10000000)]
+ },
+ [
+ "made 1/1 records static on ts-1000000.zone.",
+ "made 1/1 records static on ts-100.zone.",
+ "made 2/4 records static on ts-multi.zone.",
+ ],
+ False
+ ),
+ (
+ # no change, old records already static
+ ['--mark-old-records-static=1972-03-04'],
+ {},
+ [],
+ False
+ ),
+ (
+ # no change, samba-tool added records already static
+ ['--mark-records-static-regex=sniz'],
+ {},
+ [],
+ False
+ ),
+ (
+ # snizle has 2 A, 2 AAAA, 10 fancy, and 2 TXT records, in
+ # that order.
+ # the A, AAAA, and TXT recrods should be dynamic
+ ['--mark-records-dynamic-regex=sniz'],
+ {'snizle': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')]
+ },
+ ['made 6/16 records dynamic on snizle.zone.'],
+ False
+ ),
+ (
+ # This regex should catch snizle, weergly, and ts-*
+ # but we're doing dry-run so no change
+ ['--mark-records-dynamic-regex=[sw]', '-n'],
+ {},
+ ['would make 3/4 records dynamic on ts-multi.zone.',
+ 'would make 1/1 records dynamic on ts-0.zone.',
+ 'would make 1/1 records dynamic on ts-1000000.zone.',
+ 'would make 6/16 records dynamic on weergly.zone.',
+ 'would make 1/1 records dynamic on ts-100.zone.'
+ ],
+ False
+ ),
+ (
+ # This regex should catch snizle and frobitz
+ # but snizle has already been changed.
+ ['--mark-records-dynamic-regex=z'],
+ {'frobitz': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')]
+ },
+ ['made 6/16 records dynamic on frobitz.zone.'],
+ False
+ ),
+ (
+ # This regex should catch snizle, frobitz, and
+ # ts-multi. Note that the 1e7 ts-multi record is
+ # alreay dynamic and doesn't change.
+ ['--mark-records-dynamic-regex=[i]'],
+ {'ts-multi': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 10000000)]
+ },
+ ['made 3/4 records dynamic on ts-multi.zone.'],
+ False
+ ),
+ (
+ # matches no records
+ ['--mark-records-dynamic-regex=^aloooooo[qw]+'],
+ {},
+ [],
+ False
+ ),
+ (
+ # This should be an error, as only one --mark-*
+ # argument is allowed at a time
+ ['--mark-records-dynamic-regex=.',
+ '--mark-records-static-regex=.',
+ ],
+ {},
+ [],
+ True
+ ),
+ (
+ # This should also be an error
+ ['--mark-old-records-static=1997-07-07',
+ '--mark-records-static-regex=.',
+ ],
+ {},
+ [],
+ True
+ ),
+ (
+ # This should not be an error. --aging and refresh
+ # options can be mixed with --mark ones.
+ ['--mark-old-records-static=1997-07-07',
+ '--aging=0',
+ ],
+ {},
+ ['Set Aging to 0'],
+ False
+ ),
+ (
+ # This regex should catch weergly, but all the
+ # records are already static,
+ ['--mark-records-static-regex=wee'],
+ {},
+ [],
+ False
+ ),
+ (
+ # Make frobitz static again.
+ ['--mark-records-static-regex=obi'],
+ {'frobitz': [('R', 0),
+ ('R', 0),
+ ('R', 0),
+ ('R', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 0),
+ ('R', 0)]
+ },
+ ['made 6/16 records static on frobitz.zone.'],
+ False
+ ),
+ (
+ # would make almost everything static, but --dry-run
+ ['--mark-old-records-static=2222-03-04', '--dry-run'],
+ {},
+ [
+ 'would make 6/16 records static on snizle.zone.',
+ 'would make 3/4 records static on ts-multi.zone.'
+ ],
+ False
+ ),
+ (
+ # make everything static
+ ['--mark-records-static-regex=.'],
+ {'snizle': [('R', 0),
+ ('R', 0),
+ ('R', 0),
+ ('R', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 0),
+ ('R', 0)],
+ 'ts-10000000': [('R', 0)],
+ 'ts-multi': [('R', 0), ('R', 0), ('R', 0), ('R', 0)]
+ },
+ [
+ 'made 4/4 records static on ts-multi.zone.',
+ 'made 1/1 records static on ts-10000000.zone.',
+ 'made 6/16 records static on snizle.zone.',
+ ],
+ False
+ ),
+ (
+ # make everything dynamic that can be
+ ['--mark-records-dynamic-regex=.'],
+ {'frobitz': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')],
+ 'snizle': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')],
+ 'ts-0': [('R', 'nowish')],
+ 'ts-100': [('R', 'nowish')],
+ 'ts-1000000': [('R', 'nowish')],
+ 'ts-10000000': [('R', 'nowish')],
+ 'ts-multi': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish')],
+ 'weergly': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')]
+ },
+ [
+ 'made 4/4 records dynamic on ts-multi.zone.',
+ 'made 6/16 records dynamic on snizle.zone.',
+ 'made 1/1 records dynamic on ts-0.zone.',
+ 'made 1/1 records dynamic on ts-1000000.zone.',
+ 'made 1/1 records dynamic on ts-10000000.zone.',
+ 'made 1/1 records dynamic on ts-100.zone.',
+ 'made 6/16 records dynamic on frobitz.zone.',
+ 'made 6/16 records dynamic on weergly.zone.',
+ ],
+ False
+ ),
+ ):
+ result, out, err = self.runsubcmd("dns",
+ "zoneoptions",
+ os.environ["SERVER"],
+ self.zone,
+ self.creds_string,
+ *options)
+ if error:
+ self.assertCmdFail(result, f"zoneoptions should fail ({error})")
+ else:
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "zoneoptions shouldn't fail")
+
+ new_tsmap = self.get_timestamp_map()
+
+ # same keys, always
+ self.assertEqual(sorted(new_tsmap), sorted(tsmap))
+ changes = {}
+ for k in tsmap:
+ if tsmap[k] != new_tsmap[k]:
+ changes[k] = new_tsmap[k]
+
+ self.assertEqual(diff, changes)
+
+ for s in output_substrings:
+ self.assertIn(s, out)
+ tsmap = new_tsmap