1
0
mirror of https://github.com/samba-team/samba.git synced 2025-08-04 08:22:08 +03:00

samba-tool dns zoneoptions: timestamp manipulation options

There was a bug in Samba before 4.9 that marked all records intended
to be static with a current timestamp, and all records intended to be
dynamic with a zero timestamp. This was exactly the opposite of
correct behaviour.

It follows that a domain which has been upgraded past 4.9, but on
which aging is not enabled, records intended to be static will have a
timestamp from before the upgrade date (unless their nodes have
suffered a DNS update, which due to another bug, will change the
timestmap). The following command will make these truly static:

$ samba-tool dns zoneoptions --mark-old-records-static=2018-07-23 -U...

where '2018-07-23' should be replaced by the approximate date of the
upgrade beyond 4.9.

It seems riskier making blanket conversions of static records into
dynamic records, but there are sometimes useful patterns in the names
given to machines that we can exploit. For example, if there is a
group of machines with names like 'desktop-123' that are all supposed
to using dynamic DNS, the adminstrator can go

$ samba-tool dns zoneoptions --mark-records-dynamic-regex='desktop-\d+'

and there's a --mark-records-static-regex for symmetry.

These options are deliberately long and cumbersome to type, so people
have a chance to think before they get to the end. We also introduce a
'--dry-run' (or '-n') option so they can inspect the likely results
before going ahead.

*NOTE* ageing will still not work properly after this commit, due to
other bugs that will be fixed in other commits.

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
This commit is contained in:
Douglas Bagnall
2021-05-27 09:46:02 +12:00
committed by Douglas Bagnall
parent 074f9e1486
commit 2f7aa81a9f
2 changed files with 649 additions and 3 deletions

View File

@ -23,8 +23,12 @@ from socket import inet_ntop
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
@ -452,6 +456,14 @@ class cmd_zoneoptions(Command):
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 = []
@ -476,7 +488,11 @@ class cmd_zoneoptions(Command):
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)
@ -496,6 +512,9 @@ class cmd_zoneoptions(Command):
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,
@ -510,6 +529,187 @@ class cmd_zoneoptions(Command):
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."""