s4:dsdb: Fix stack use after scope in gkdi_create_root_key()
[samba.git] / source4 / scripting / bin / samba_dnsupdate
1 #!/usr/bin/env python3
2 # vim: expandtab
3 #
4 # update our DNS names using TSIG-GSS
5 #
6 # Copyright (C) Andrew Tridgell 2010
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
22 import os
23 import fcntl
24 import sys
25 import tempfile
26 import subprocess
27
28 # ensure we get messages out immediately, so they get in the samba logs,
29 # and don't get swallowed by a timeout
30 os.environ['PYTHONUNBUFFERED'] = '1'
31
32 # forcing GMT avoids a problem in some timezones with kerberos. Both MIT and
33 # heimdal can get mutual authentication errors due to the 24 second difference
34 # between UTC and GMT when using some zone files (eg. the PDT zone from
35 # the US)
36 os.environ["TZ"] = "GMT"
37
38 # Find right directory when running from source tree
39 sys.path.insert(0, "bin/python")
40
41 import samba
42 import optparse
43 from samba import getopt as options
44 from ldb import SCOPE_BASE
45 from samba import dsdb
46 from samba.auth import system_session
47 from samba.samdb import SamDB
48 from samba.dcerpc import netlogon, winbind
49 from samba.netcmd.dns import cmd_dns
50 from samba import gensec
51 from samba.kcc import kcc_utils
52 from samba.common import get_string
53 import ldb
54
55 from samba.dnsresolver import DNSResolver
56 import dns.resolver
57 import dns.exception
58
59 default_ttl = 900
60 am_rodc = False
61 error_count = 0
62
63 parser = optparse.OptionParser("samba_dnsupdate [options]")
64 sambaopts = options.SambaOptions(parser)
65 parser.add_option_group(sambaopts)
66 parser.add_option_group(options.VersionOptions(parser))
67 parser.add_option("--verbose", action="store_true")
68 parser.add_option("--use-samba-tool", action="store_true", help="Use samba-tool to make updates over RPC, rather than over DNS")
69 parser.add_option("--use-nsupdate", action="store_true", help="Use nsupdate command to make updates over DNS (default, if kinit successful)")
70 parser.add_option("--all-names", action="store_true")
71 parser.add_option("--all-interfaces", action="store_true")
72 parser.add_option("--current-ip", action="append", help="IP address to update DNS to match (helpful if behind NAT, valid multiple times, defaults to values from interfaces=)")
73 parser.add_option("--rpc-server-ip", type="string", help="IP address of server to use with samba-tool (defaults to first --current-ip)")
74 parser.add_option("--use-file", type="string", help="Use a file, rather than real DNS calls")
75 parser.add_option("--update-list", type="string", help="Add DNS names from the given file")
76 parser.add_option("--update-cache", type="string", help="Cache database of already registered records")
77 parser.add_option("--fail-immediately", action='store_true', help="Exit on first failure")
78 parser.add_option("--no-credentials", dest='nocreds', action='store_true', help="don't try and get credentials")
79 parser.add_option("--no-substitutions", dest='nosubs', action='store_true', help="don't try and expands variables in file specified by --update-list")
80
81 creds = None
82 ccachename = None
83
84 opts, args = parser.parse_args()
85
86 if len(args) != 0:
87     parser.print_usage()
88     sys.exit(1)
89
90 lp = sambaopts.get_loadparm()
91
92 domain = lp.get("realm")
93 host = lp.get("netbios name")
94 all_interfaces = opts.all_interfaces
95
96 IPs = opts.current_ip or samba.interface_ips(lp, bool(all_interfaces)) or []
97
98 nsupdate_cmd = lp.get('nsupdate command')
99 dns_zone_scavenging = lp.get("dns zone scavenging")
100
101 if len(IPs) == 0:
102     print("No IP interfaces - skipping DNS updates\n")
103     parser.print_usage()
104     sys.exit(0)
105
106 rpc_server_ip = opts.rpc_server_ip or IPs[0]
107
108 IP6s = [ip for ip in IPs if ':' in ip]
109 IP4s = [ip for ip in IPs if ':' not in ip]
110
111 smb_conf = sambaopts.get_loadparm_path()
112
113 if opts.verbose:
114     print("IPs: %s" % IPs)
115
116 def get_possible_rw_dns_server(creds, domain):
117     """Get a list of possible read-write DNS servers, starting with
118        the SOA.  The SOA is the correct answer, but old Samba domains
119        (4.6 and prior) do not maintain this value, so add NS servers
120        as well"""
121
122     ans_soa = check_one_dns_name(domain, 'SOA')
123     # Actually there is only one
124     hosts_soa = [str(a.mname).rstrip('.') for a in ans_soa]
125
126     # This is not strictly legit, but old Samba domains may have an
127     # unmaintained SOA record, so go for any NS that we can get a
128     # ticket to.
129     ans_ns = check_one_dns_name(domain, 'NS')
130     # Actually there is only one
131     hosts_ns = [str(a.target).rstrip('.') for a in ans_ns]
132
133     return hosts_soa + hosts_ns
134
135 def get_krb5_rw_dns_server(creds, domain):
136     """Get a list of read-write DNS servers that we can obtain a ticket
137        to, starting with the SOA.  The SOA is the correct answer, but
138        old Samba domains (4.6 and prior) do not maintain this value,
139        so continue with the NS servers as well until we get one that
140        the KDC will issue a ticket to.
141     """
142
143     rw_dns_servers = get_possible_rw_dns_server(creds, domain)
144     # Actually there is only one
145     for i, target_hostname in enumerate(rw_dns_servers):
146         settings = {}
147         settings["lp_ctx"] = lp
148         settings["target_hostname"] = target_hostname
149
150         gensec_client = gensec.Security.start_client(settings)
151         gensec_client.set_credentials(creds)
152         gensec_client.set_target_service("DNS")
153         gensec_client.set_target_hostname(target_hostname)
154         gensec_client.want_feature(gensec.FEATURE_SEAL)
155         gensec_client.start_mech_by_sasl_name("GSSAPI")
156         server_to_client = b""
157         try:
158             (client_finished, client_to_server) = gensec_client.update(server_to_client)
159             if opts.verbose:
160                 print("Successfully obtained Kerberos ticket to DNS/%s as %s" \
161                     % (target_hostname, creds.get_username()))
162             return target_hostname
163         except RuntimeError:
164             # Only raise an exception if they all failed
165             if i == len(rw_dns_servers) - 1:
166                 raise
167
168 def get_credentials(lp):
169     """# get credentials if we haven't got them already."""
170     from samba import credentials
171     global ccachename
172     creds = credentials.Credentials()
173     creds.guess(lp)
174     creds.set_machine_account(lp)
175     creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE)
176     (tmp_fd, ccachename) = tempfile.mkstemp()
177     try:
178         if opts.use_file is not None:
179             return
180
181         creds.get_named_ccache(lp, ccachename)
182
183         # Now confirm we can get a ticket to the DNS server
184         get_krb5_rw_dns_server(creds, sub_vars['DNSDOMAIN'] + '.')
185         return creds
186
187     except RuntimeError as e:
188         os.unlink(ccachename)
189         raise e
190
191
192 class dnsobj(object):
193     """an object to hold a parsed DNS line"""
194
195     def __init__(self, string_form):
196         list = string_form.split()
197         if len(list) < 3:
198             raise Exception("Invalid DNS entry %r" % string_form)
199         self.dest = None
200         self.port = None
201         self.ip = None
202         self.existing_port = None
203         self.existing_weight = None
204         self.existing_cname_target = None
205         self.rpc = False
206         self.zone = None
207         if list[0] == "RPC":
208             self.rpc = True
209             self.zone = list[1]
210             list = list[2:]
211         self.type = list[0]
212         self.name = list[1]
213         self.nameservers = []
214         if self.type == 'SRV':
215             if len(list) < 4:
216                 raise Exception("Invalid DNS entry %r" % string_form)
217             self.dest = list[2]
218             self.port = list[3]
219         elif self.type in ['A', 'AAAA']:
220             self.ip   = list[2] # usually $IP, which gets replaced
221         elif self.type == 'CNAME':
222             self.dest = list[2]
223         elif self.type == 'NS':
224             self.dest = list[2]
225         else:
226             raise Exception("Received unexpected DNS reply of type %s: %s" % (self.type, string_form))
227
228     def __str__(self):
229         if self.type == "A":
230             return "%s %s %s" % (self.type, self.name, self.ip)
231         if self.type == "AAAA":
232             return "%s %s %s" % (self.type, self.name, self.ip)
233         if self.type == "SRV":
234             return "%s %s %s %s" % (self.type, self.name, self.dest, self.port)
235         if self.type == "CNAME":
236             return "%s %s %s" % (self.type, self.name, self.dest)
237         if self.type == "NS":
238             return "%s %s %s" % (self.type, self.name, self.dest)
239
240
241 def parse_dns_line(line, sub_vars):
242     """parse a DNS line from."""
243     if line.startswith("SRV _ldap._tcp.pdc._msdcs.") and not samdb.am_pdc():
244         # We keep this as compat to the dns_update_list of 4.0/4.1
245         if opts.verbose:
246             print("Skipping PDC entry (%s) as we are not a PDC" % line)
247         return None
248     subline = samba.substitute_var(line, sub_vars)
249     if subline == '' or subline[0] == "#":
250         return None
251     return dnsobj(subline)
252
253
254 def hostname_match(h1, h2):
255     """see if two hostnames match."""
256     h1 = str(h1)
257     h2 = str(h2)
258     return h1.lower().rstrip('.') == h2.lower().rstrip('.')
259
260 def get_resolver(d=None):
261     resolv_conf = os.getenv('RESOLV_CONF', default='/etc/resolv.conf')
262     resolver = DNSResolver(filename=resolv_conf, configure=True)
263
264     if d is not None and d.nameservers != []:
265         resolver.nameservers = d.nameservers
266
267     return resolver
268
269 def check_one_dns_name(name, name_type, d=None):
270     resolver = get_resolver(d)
271     if d and not d.nameservers:
272         d.nameservers = resolver.nameservers
273     # dns.resolver.Answer
274     return resolver.resolve(name, name_type)
275
276 def check_dns_name(d):
277     """check that a DNS entry exists."""
278     normalised_name = d.name.rstrip('.') + '.'
279     if opts.verbose:
280         print("Looking for DNS entry %s as %s" % (d, normalised_name))
281
282     if opts.use_file is not None:
283         try:
284             dns_file = open(opts.use_file, "r")
285         except IOError:
286             return False
287
288         with dns_file:
289             for line in dns_file:
290                 line = line.strip()
291                 if line == '' or line[0] == "#":
292                     continue
293                 if line.lower() == str(d).lower():
294                     return True
295         return False
296
297     try:
298         ans = check_one_dns_name(normalised_name, d.type, d)
299     except dns.exception.Timeout:
300         raise Exception("Timeout while waiting to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
301     except dns.resolver.NoNameservers:
302         raise Exception("Unable to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
303     except dns.resolver.NXDOMAIN:
304         if opts.verbose:
305             print("The DNS entry %s, queried as %s does not exist" % (d, normalised_name))
306         return False
307     except dns.resolver.NoAnswer:
308         if opts.verbose:
309             print("The DNS entry %s, queried as %s does not hold this record type" % (d, normalised_name))
310         return False
311     except dns.exception.DNSException:
312         raise Exception("Failure while trying to resolve %s as %s" % (d, normalised_name))
313     if d.type in ['A', 'AAAA']:
314         # we need to be sure that our IP is there
315         for rdata in ans:
316             if str(rdata) == str(d.ip):
317                 return True
318     elif d.type == 'CNAME':
319         for i in range(len(ans)):
320             if hostname_match(ans[i].target, d.dest):
321                 return True
322             else:
323                 d.existing_cname_target = str(ans[i].target)
324     elif d.type == 'NS':
325         for i in range(len(ans)):
326             if hostname_match(ans[i].target, d.dest):
327                 return True
328     elif d.type == 'SRV':
329         for rdata in ans:
330             if opts.verbose:
331                 print("Checking %s against %s" % (rdata, d))
332             if hostname_match(rdata.target, d.dest):
333                 if str(rdata.port) == str(d.port):
334                     return True
335                 else:
336                     d.existing_port     = str(rdata.port)
337                     d.existing_weight = str(rdata.weight)
338
339     if opts.verbose:
340         print("Lookup of %s succeeded, but we failed to find a matching DNS entry for %s" % (normalised_name, d))
341
342     return False
343
344
345 def get_subst_vars(samdb):
346     """get the list of substitution vars."""
347     global lp, am_rodc
348     vars = {}
349
350     vars['DNSDOMAIN'] = samdb.domain_dns_name()
351     vars['DNSFOREST'] = samdb.forest_dns_name()
352     vars['HOSTNAME']  = samdb.host_dns_name()
353     vars['NTDSGUID']  = samdb.get_ntds_GUID()
354     vars['SITE']      = samdb.server_site_name()
355     res = samdb.search(base=samdb.get_default_basedn(), scope=SCOPE_BASE, attrs=["objectGUID"])
356     guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0])
357     vars['DOMAINGUID'] = get_string(guid)
358
359     vars['IF_DC'] = ""
360     vars['IF_RWDC'] = "# "
361     vars['IF_RODC'] = "# "
362     vars['IF_PDC'] = "# "
363     vars['IF_GC'] = "# "
364     vars['IF_RWGC'] = "# "
365     vars['IF_ROGC'] = "# "
366     vars['IF_DNS_DOMAIN'] = "# "
367     vars['IF_RWDNS_DOMAIN'] = "# "
368     vars['IF_RODNS_DOMAIN'] = "# "
369     vars['IF_DNS_FOREST'] = "# "
370     vars['IF_RWDNS_FOREST'] = "# "
371     vars['IF_R0DNS_FOREST'] = "# "
372
373     am_rodc = samdb.am_rodc()
374     if am_rodc:
375         vars['IF_RODC'] = ""
376     else:
377         vars['IF_RWDC'] = ""
378
379     if samdb.am_pdc():
380         vars['IF_PDC'] = ""
381
382     # check if we "are DNS server"
383     res = samdb.search(base=samdb.get_config_basedn(),
384                    expression='(objectguid=%s)' % vars['NTDSGUID'],
385                    attrs=["options", "msDS-hasMasterNCs"])
386
387     if len(res) == 1:
388         if "options" in res[0]:
389             options = int(res[0]["options"][0])
390             if (options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
391                 vars['IF_GC'] = ""
392                 if am_rodc:
393                     vars['IF_ROGC'] = ""
394                 else:
395                     vars['IF_RWGC'] = ""
396
397         basedn = str(samdb.get_default_basedn())
398         forestdn = str(samdb.get_root_basedn())
399
400         if "msDS-hasMasterNCs" in res[0]:
401             for e in res[0]["msDS-hasMasterNCs"]:
402                 if str(e) == "DC=DomainDnsZones,%s" % basedn:
403                     vars['IF_DNS_DOMAIN'] = ""
404                     if am_rodc:
405                         vars['IF_RODNS_DOMAIN'] = ""
406                     else:
407                         vars['IF_RWDNS_DOMAIN'] = ""
408                 if str(e) == "DC=ForestDnsZones,%s" % forestdn:
409                     vars['IF_DNS_FOREST'] = ""
410                     if am_rodc:
411                         vars['IF_RODNS_FOREST'] = ""
412                     else:
413                         vars['IF_RWDNS_FOREST'] = ""
414
415     return vars
416
417
418 def call_nsupdate(d, op="add"):
419     """call nsupdate for an entry."""
420     global ccachename, nsupdate_cmd, krb5conf
421
422     assert(op in ["add", "delete"])
423
424     if opts.use_file is not None:
425         if opts.verbose:
426             print("Use File instead of nsupdate for %s (%s)" % (d, op))
427
428         try:
429             rfile = open(opts.use_file, 'r+')
430         except IOError:
431             # Perhaps create it
432             open(opts.use_file, 'w+').close()
433             # Open it for reading again, in case someone else got to it first
434             rfile = open(opts.use_file, 'r+')
435         fcntl.lockf(rfile, fcntl.LOCK_EX)
436         (file_dir, file_name) = os.path.split(opts.use_file)
437         (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
438         wfile = os.fdopen(tmp_fd, 'a')
439         for line in rfile:
440             if op == "delete":
441                 l = parse_dns_line(line, {})
442                 if str(l).lower() == str(d).lower():
443                     continue
444             wfile.write(line)
445         if op == "add":
446             wfile.write(str(d)+"\n")
447         rfile.close()
448         wfile.close()
449         os.rename(tmpfile, opts.use_file)
450         return
451
452     if opts.verbose:
453         print("Calling nsupdate for %s (%s)" % (d, op))
454
455     normalised_name = d.name.rstrip('.') + '.'
456
457     (tmp_fd, tmpfile) = tempfile.mkstemp()
458     f = os.fdopen(tmp_fd, 'w')
459
460     resolver = get_resolver(d)
461
462     # Local the zone for this name
463     zone = dns.resolver.zone_for_name(normalised_name,
464                                       resolver=resolver)
465
466     # Now find the SOA, or if we can't get a ticket to the SOA,
467     # any server with an NS record we can get a ticket for.
468     #
469     # Thanks to the Kerberos Credentials cache this is not
470     # expensive inside the loop
471     server = get_krb5_rw_dns_server(creds, zone)
472     f.write('server %s\n' % server)
473
474     if d.type == "A":
475         f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
476     if d.type == "AAAA":
477         f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
478     if d.type == "SRV":
479         if op == "add" and d.existing_port is not None:
480             f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
481                                                            d.existing_port, d.dest))
482         f.write("update %s %s %u SRV 0 100 %s %s\n" % (op, normalised_name, default_ttl, d.port, d.dest))
483     if d.type == "CNAME":
484         f.write("update %s %s %u CNAME %s\n" % (op, normalised_name, default_ttl, d.dest))
485     if d.type == "NS":
486         f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
487     if opts.verbose:
488         f.write("show\n")
489     f.write("send\n")
490     f.close()
491
492     # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
493     os.environ["SOCKET_WRAPPER_MTU"] = "2000"
494
495     global error_count
496     if ccachename:
497         os.environ["KRB5CCNAME"] = ccachename
498     try:
499         cmd = nsupdate_cmd[:]
500         cmd.append(tmpfile)
501         env = os.environ
502         if krb5conf:
503             env["KRB5_CONFIG"] = krb5conf
504         if ccachename:
505             env["KRB5CCNAME"] = ccachename
506         ret = subprocess.call(cmd, shell=False, env=env)
507         if ret != 0:
508             if opts.fail_immediately:
509                 if opts.verbose:
510                     print("Failed update with %s" % tmpfile)
511                 sys.exit(1)
512             error_count = error_count + 1
513             if opts.verbose:
514                 print("Failed nsupdate: %d" % ret)
515     except Exception as estr:
516         if opts.fail_immediately:
517             sys.exit(1)
518         error_count = error_count + 1
519         if opts.verbose:
520             print("Failed nsupdate: %s : %s" % (str(d), estr))
521     os.unlink(tmpfile)
522
523     # Let socket_wrapper set the default MTU size
524     os.environ["SOCKET_WRAPPER_MTU"] = "0"
525
526
527 def call_samba_tool(d, op="add", zone=None):
528     """call samba-tool dns to update an entry."""
529
530     assert(op in ["add", "delete"])
531
532     if (sub_vars['DNSFOREST'] != sub_vars['DNSDOMAIN']) and \
533        sub_vars['DNSFOREST'].endswith('.' + sub_vars['DNSDOMAIN']):
534         print("Refusing to use samba-tool when forest %s is under domain %s" \
535             % (sub_vars['DNSFOREST'], sub_vars['DNSDOMAIN']))
536
537     if opts.verbose:
538         print("Calling samba-tool dns for %s (%s)" % (d, op))
539
540     normalised_name = d.name.rstrip('.') + '.'
541     if zone is None:
542         if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
543             short_name = '@'
544             zone = sub_vars['DNSDOMAIN']
545         elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
546             short_name = '@'
547             zone = sub_vars['DNSFOREST']
548         elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
549             short_name = '@'
550             zone = '_msdcs.' + sub_vars['DNSFOREST']
551         else:
552             if not normalised_name.endswith('.' + sub_vars['DNSDOMAIN'] + '.'):
553                 print("Not Calling samba-tool dns for %s (%s), %s not in %s" % (d, op, normalised_name, sub_vars['DNSDOMAIN'] + '.'))
554                 return False
555             elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
556                 zone = '_msdcs.' + sub_vars['DNSFOREST']
557             else:
558                 zone = sub_vars['DNSDOMAIN']
559             len_zone = len(zone)+2
560             short_name = normalised_name[:-len_zone]
561     else:
562         len_zone = len(zone)+2
563         short_name = normalised_name[:-len_zone]
564
565     if d.type == "A":
566         args = [rpc_server_ip, zone, short_name, "A", d.ip]
567     if d.type == "AAAA":
568         args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
569     if d.type == "SRV":
570         if op == "add" and d.existing_port is not None:
571             print("Not handling modify of existing SRV %s using samba-tool" % d)
572             return False
573         args = [rpc_server_ip, zone, short_name, "SRV",
574                 "%s %s %s %s" % (d.dest, d.port, "0", "100")]
575     if d.type == "CNAME":
576         if d.existing_cname_target is None:
577             args = [rpc_server_ip, zone, short_name, "CNAME", d.dest]
578         else:
579             op = "update"
580             args = [rpc_server_ip, zone, short_name, "CNAME",
581                     d.existing_cname_target.rstrip('.'), d.dest]
582
583     if d.type == "NS":
584         args = [rpc_server_ip, zone, short_name, "NS", d.dest]
585
586     if smb_conf and args:
587         args += ["--configfile=" + smb_conf]
588
589     global error_count
590     try:
591         cmd = cmd_dns()
592         if opts.verbose:
593             print(f'Calling samba-tool dns {op} --use-kerberos off -P {args}')
594         command, args = cmd._resolve("dns", op, "--use-kerberos", "off", "-P", *args)
595         ret = command._run(*args)
596         if ret == -1:
597             if opts.fail_immediately:
598                 sys.exit(1)
599             error_count = error_count + 1
600             if opts.verbose:
601                 print("Failed 'samba-tool dns' based update of %s" % (str(d)))
602     except Exception as estr:
603         if opts.fail_immediately:
604             sys.exit(1)
605         error_count = error_count + 1
606         if opts.verbose:
607             print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
608         raise
609
610 irpc_wb = None
611 def cached_irpc_wb(lp):
612     global irpc_wb
613     if irpc_wb is not None:
614         return irpc_wb
615     irpc_wb = winbind.winbind("irpc:winbind_server", lp)
616     return irpc_wb
617
618 def rodc_dns_update(d, t, op):
619     '''a single DNS update via the RODC netlogon call'''
620     global sub_vars
621
622     assert(op in ["add", "delete"])
623
624     if opts.verbose:
625         print("Calling netlogon RODC update for %s" % d)
626
627     typemap = {
628         netlogon.NlDnsLdapAtSite       : netlogon.NlDnsInfoTypeNone,
629         netlogon.NlDnsGcAtSite         : netlogon.NlDnsDomainNameAlias,
630         netlogon.NlDnsDsaCname         : netlogon.NlDnsDomainNameAlias,
631         netlogon.NlDnsKdcAtSite        : netlogon.NlDnsInfoTypeNone,
632         netlogon.NlDnsDcAtSite         : netlogon.NlDnsInfoTypeNone,
633         netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
634         netlogon.NlDnsGenericGcAtSite  : netlogon.NlDnsDomainNameAlias
635         }
636
637     w = cached_irpc_wb(lp)
638     dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
639     dns_names.count = 1
640     name = netlogon.NL_DNS_NAME_INFO()
641     name.type = t
642     name.dns_domain_info_type = typemap[t]
643     name.priority = 0
644     name.weight   = 0
645     if d.port is not None:
646         name.port = int(d.port)
647     if op == "add":
648         name.dns_register = True
649     else:
650         name.dns_register = False
651     dns_names.names = [ name ]
652     site_name = sub_vars['SITE']
653
654     global error_count
655
656     try:
657         ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
658         if ret_names.names[0].status != 0:
659             print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
660             error_count = error_count + 1
661     except RuntimeError as reason:
662         print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
663         error_count = error_count + 1
664
665     if opts.verbose:
666         print("Called netlogon RODC update for %s" % d)
667
668     if error_count != 0 and opts.fail_immediately:
669         sys.exit(1)
670
671
672 def call_rodc_update(d, op="add"):
673     '''RODCs need to use the netlogon API for nsupdate'''
674     global lp, sub_vars
675
676     assert(op in ["add", "delete"])
677
678     # we expect failure for 3268 if we aren't a GC
679     if d.port is not None and int(d.port) == 3268:
680         return
681
682     # map the DNS request to a netlogon update type
683     map = {
684         netlogon.NlDnsLdapAtSite       : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
685         netlogon.NlDnsGcAtSite         : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
686         netlogon.NlDnsDsaCname         : '${NTDSGUID}._msdcs.${DNSFOREST}',
687         netlogon.NlDnsKdcAtSite        : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
688         netlogon.NlDnsDcAtSite         : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
689         netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
690         netlogon.NlDnsGenericGcAtSite  : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
691         }
692
693     for t in map:
694         subname = samba.substitute_var(map[t], sub_vars)
695         if subname.lower() == d.name.lower():
696             # found a match - do the update
697             rodc_dns_update(d, t, op)
698             return
699     if opts.verbose:
700         print("Unable to map to netlogon DNS update: %s" % d)
701
702
703 # get the list of DNS entries we should have
704 dns_update_list = opts.update_list or lp.private_path('dns_update_list')
705
706 dns_update_cache = opts.update_cache or lp.private_path('dns_update_cache')
707
708 krb5conf = None
709 # only change the krb5.conf if we are not in selftest
710 if 'SOCKET_WRAPPER_DIR' not in os.environ:
711     # use our private krb5.conf to avoid problems with the wrong domain
712     # bind9 nsupdate wants the default domain set
713     krb5conf = lp.private_path('krb5.conf')
714     os.environ['KRB5_CONFIG'] = krb5conf
715
716 try:
717     file = open(dns_update_list, "r")
718 except OSError as e:
719     if opts.update_list:
720         print("The specified update list does not exist")
721     else:
722         print("The server update list was not found, "
723               "and --update-list was not provided.")
724     print(e)
725     print()
726     parser.print_usage()
727     sys.exit(1)
728
729 if opts.nosubs:
730     sub_vars = {}
731 else:
732     samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
733
734     # get the substitution dictionary
735     sub_vars = get_subst_vars(samdb)
736
737 # build up a list of update commands to pass to nsupdate
738 update_list = []
739 dns_list = []
740 cache_list = []
741 delete_list = []
742
743 dup_set = set()
744 cache_set = set()
745
746 rebuild_cache = False
747 try:
748     cfile = open(dns_update_cache, 'r+')
749 except IOError:
750     # Perhaps create it
751     open(dns_update_cache, 'w+').close()
752     # Open it for reading again, in case someone else got to it first
753     cfile = open(dns_update_cache, 'r+')
754 fcntl.lockf(cfile, fcntl.LOCK_EX)
755 for line in cfile:
756     line = line.strip()
757     if line == '' or line[0] == "#":
758         continue
759     c = parse_dns_line(line, {})
760     if c is None:
761         continue
762     if str(c) not in cache_set:
763         cache_list.append(c)
764         cache_set.add(str(c))
765
766 cfile.close()
767
768 site_specific_rec = []
769
770 # read each line, and check that the DNS name exists
771 for line in file:
772     line = line.strip()
773
774     if '${SITE}' in line:
775         site_specific_rec.append(line)
776
777     if line == '' or line[0] == "#":
778         continue
779     d = parse_dns_line(line, sub_vars)
780     if d is None:
781         continue
782     if d.type == 'A' and len(IP4s) == 0:
783         continue
784     if d.type == 'AAAA' and len(IP6s) == 0:
785         continue
786     if str(d) not in dup_set:
787         dns_list.append(d)
788         dup_set.add(str(d))
789
790 file.close()
791
792 # Perform automatic site coverage by default
793 auto_coverage = True
794
795 if not am_rodc and auto_coverage:
796     site_names = kcc_utils.uncovered_sites_to_cover(samdb,
797                                                     samdb.server_site_name())
798
799     # Duplicate all site specific records for the uncovered site
800     for site in site_names:
801         to_add = [samba.substitute_var(line, {'SITE': site})
802                   for line in site_specific_rec]
803
804         for site_line in to_add:
805             d = parse_dns_line(site_line,
806                                sub_vars=sub_vars)
807             if d is not None and str(d) not in dup_set:
808                 dns_list.append(d)
809                 dup_set.add(str(d))
810
811 # now expand the entries, if any are A record with ip set to $IP
812 # then replace with multiple entries, one for each interface IP
813 for d in dns_list:
814     if d.ip != "$IP":
815         continue
816     if d.type == 'A':
817         d.ip = IP4s[0]
818         for i in range(len(IP4s)-1):
819             d2 = dnsobj(str(d))
820             d2.ip = IP4s[i+1]
821             dns_list.append(d2)
822     if d.type == 'AAAA':
823         d.ip = IP6s[0]
824         for i in range(len(IP6s)-1):
825             d2 = dnsobj(str(d))
826             d2.ip = IP6s[i+1]
827             dns_list.append(d2)
828
829 # now check if the entries already exist on the DNS server
830 for d in dns_list:
831     found = False
832     for c in cache_list:
833         if str(c).lower() == str(d).lower():
834             found = True
835             break
836     if not found:
837         rebuild_cache = True
838         if opts.verbose:
839             print("need cache add: %s" % d)
840     if dns_zone_scavenging:
841         update_list.append(d)
842         if opts.verbose:
843             print("scavenging requires update: %s" % d)
844     elif opts.all_names:
845         update_list.append(d)
846         if opts.verbose:
847             print("force update: %s" % d)
848     elif not check_dns_name(d):
849         update_list.append(d)
850         if opts.verbose:
851             print("need update: %s" % d)
852
853 for c in cache_list:
854     found = False
855     for d in dns_list:
856         if str(c).lower() == str(d).lower():
857             found = True
858             break
859     if found:
860         continue
861     rebuild_cache = True
862     if opts.verbose:
863         print("need cache remove: %s" % c)
864     if not opts.all_names and not check_dns_name(c):
865         continue
866     delete_list.append(c)
867     if opts.verbose:
868         print("need delete: %s" % c)
869
870 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
871     if opts.verbose:
872         print("No DNS updates needed")
873     sys.exit(0)
874 else:
875     if opts.verbose:
876         print("%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list)))
877
878 use_samba_tool = opts.use_samba_tool
879 use_nsupdate = opts.use_nsupdate
880 # get our krb5 creds
881 if (delete_list or update_list) and not opts.nocreds:
882     try:
883         creds = get_credentials(lp)
884     except RuntimeError as e:
885         ccachename = None
886
887         if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
888             raise
889
890         if use_nsupdate:
891             raise
892
893         print("Failed to get Kerberos credentials, falling back to samba-tool: %s" % e)
894         use_samba_tool = True
895
896
897 # ask nsupdate to delete entries as needed
898 for d in delete_list:
899     if d.rpc or (not use_nsupdate and use_samba_tool):
900         if opts.verbose:
901             print("delete (samba-tool): %s" % d)
902         call_samba_tool(d, op="delete", zone=d.zone)
903
904     elif am_rodc:
905         if d.name.lower() == domain.lower():
906             if opts.verbose:
907                 print("skip delete (rodc): %s" % d)
908             continue
909         if d.type not in [ 'A', 'AAAA' ]:
910             if opts.verbose:
911                 print("delete (rodc): %s" % d)
912             call_rodc_update(d, op="delete")
913         else:
914             if opts.verbose:
915                 print("delete (nsupdate): %s" % d)
916             call_nsupdate(d, op="delete")
917     else:
918         if opts.verbose:
919             print("delete (nsupdate): %s" % d)
920         call_nsupdate(d, op="delete")
921
922 # ask nsupdate to add entries as needed
923 for d in update_list:
924     if d.rpc or (not use_nsupdate and use_samba_tool):
925         if opts.verbose:
926             print("update (samba-tool): %s" % d)
927         call_samba_tool(d, zone=d.zone)
928
929     elif am_rodc:
930         if d.name.lower() == domain.lower():
931             if opts.verbose:
932                 print("skip (rodc): %s" % d)
933             continue
934         if d.type not in [ 'A', 'AAAA' ]:
935             if opts.verbose:
936                 print("update (rodc): %s" % d)
937             call_rodc_update(d)
938         else:
939             if opts.verbose:
940                 print("update (nsupdate): %s" % d)
941             call_nsupdate(d)
942     else:
943         if opts.verbose:
944             print("update(nsupdate): %s" % d)
945         call_nsupdate(d)
946
947 if rebuild_cache:
948     print("Rebuilding cache at %s" % dns_update_cache)
949     (file_dir, file_name) = os.path.split(dns_update_cache)
950     (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
951     wfile = os.fdopen(tmp_fd, 'a')
952     for d in dns_list:
953         if opts.verbose:
954             print("Adding %s to %s" % (str(d), file_name))
955         wfile.write(str(d)+"\n")
956     wfile.close()
957     os.rename(tmpfile, dns_update_cache)
958
959 # delete the ccache if we created it
960 if ccachename is not None:
961     os.unlink(ccachename)
962
963 if error_count != 0:
964     print("Failed update of %u entries" % error_count)
965 sys.exit(error_count)