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
22import os
23import fcntl
24import sys
25import tempfile
26import 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
30os.environ['PYTHONUNBUFFERED'] = '1'
31
32# forcing GMT avoids a problem in some timezones with kerberos. Both MIT
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)
36os.environ["TZ"] = "GMT"
37
38# Find right directory when running from source tree
39sys.path.insert(0, "bin/python")
40
41import samba
42import optparse
43from samba import getopt as options
44from ldb import SCOPE_BASE
45from samba import dsdb
46from samba.auth import system_session
47from samba.samdb import SamDB
48from samba.dcerpc import netlogon, winbind
49from samba.netcmd.dns import cmd_dns
50from samba import gensec
51from samba.kcc import kcc_utils
52from samba.compat import get_string
53from samba.compat import text_type
54import ldb
55
56import dns.resolver
57import dns.exception
58
59default_ttl = 900
60am_rodc = False
61error_count = 0
62
63parser = optparse.OptionParser("samba_dnsupdate [options]")
64sambaopts = options.SambaOptions(parser)
65parser.add_option_group(sambaopts)
66parser.add_option_group(options.VersionOptions(parser))
67parser.add_option("--verbose", action="store_true")
68parser.add_option("--use-samba-tool", action="store_true", help="Use samba-tool to make updates over RPC, rather than over DNS")
69parser.add_option("--use-nsupdate", action="store_true", help="Use nsupdate command to make updates over DNS (default, if kinit successful)")
70parser.add_option("--all-names", action="store_true")
71parser.add_option("--all-interfaces", action="store_true")
72parser.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=)")
73parser.add_option("--rpc-server-ip", type="string", help="IP address of server to use with samba-tool (defaults to first --current-ip)")
74parser.add_option("--use-file", type="string", help="Use a file, rather than real DNS calls")
75parser.add_option("--update-list", type="string", help="Add DNS names from the given file")
76parser.add_option("--update-cache", type="string", help="Cache database of already registered records")
77parser.add_option("--fail-immediately", action='store_true', help="Exit on first failure")
78parser.add_option("--no-credentials", dest='nocreds', action='store_true', help="don't try and get credentials")
79parser.add_option("--no-substitutions", dest='nosubs', action='store_true', help="don't try and expands variables in file specified by --update-list")
80
81creds = None
82ccachename = None
83
84opts, args = parser.parse_args()
85
86if len(args) != 0:
87    parser.print_usage()
88    sys.exit(1)
89
90lp = sambaopts.get_loadparm()
91
92domain = lp.get("realm")
93host = lp.get("netbios name")
94all_interfaces = opts.all_interfaces
95
96IPs = opts.current_ip or samba.interface_ips(lp, bool(all_interfaces)) or []
97
98nsupdate_cmd = lp.get('nsupdate command')
99dns_zone_scavenging = lp.get("dns zone scavenging")
100
101if len(IPs) == 0:
102    print("No IP interfaces - skipping DNS updates\n")
103    parser.print_usage()
104    sys.exit(0)
105
106rpc_server_ip = opts.rpc_server_ip or IPs[0]
107
108IP6s = [ip for ip in IPs if ':' in ip]
109IP4s = [ip for ip in IPs if ':' not in ip]
110
111smb_conf = sambaopts.get_loadparm_path()
112
113if opts.verbose:
114    print("IPs: %s" % IPs)
115
116def 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
135def get_krb5_rw_dns_server(creds, domain):
136    """Get a list of read-write DNS servers that we can obtain a ticket
137       for, 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
168def 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
192class 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
241def 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
254def 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
260def get_resolver(d=None):
261    resolv_conf = os.getenv('RESOLV_CONF', default='/etc/resolv.conf')
262    resolver = dns.resolver.Resolver(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
269def 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.query(name, name_type)
275
276def 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        for line in dns_file:
289            line = line.strip()
290            if line == '' or line[0] == "#":
291                continue
292            if line.lower() == str(d).lower():
293                return True
294        return False
295
296    try:
297        ans = check_one_dns_name(normalised_name, d.type, d)
298    except dns.exception.Timeout:
299        raise Exception("Timeout while waiting to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
300    except dns.resolver.NoNameservers:
301        raise Exception("Unable to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
302    except dns.resolver.NXDOMAIN:
303        if opts.verbose:
304            print("The DNS entry %s, queried as %s does not exist" % (d, normalised_name))
305        return False
306    except dns.resolver.NoAnswer:
307        if opts.verbose:
308            print("The DNS entry %s, queried as %s does not hold this record type" % (d, normalised_name))
309        return False
310    except dns.exception.DNSException:
311        raise Exception("Failure while trying to resolve %s as %s" % (d, normalised_name))
312    if d.type in ['A', 'AAAA']:
313        # we need to be sure that our IP is there
314        for rdata in ans:
315            if str(rdata) == str(d.ip):
316                return True
317    elif d.type == 'CNAME':
318        for i in range(len(ans)):
319            if hostname_match(ans[i].target, d.dest):
320                return True
321            else:
322                d.existing_cname_target = str(ans[i].target)
323    elif d.type == 'NS':
324        for i in range(len(ans)):
325            if hostname_match(ans[i].target, d.dest):
326                return True
327    elif d.type == 'SRV':
328        for rdata in ans:
329            if opts.verbose:
330                print("Checking %s against %s" % (rdata, d))
331            if hostname_match(rdata.target, d.dest):
332                if str(rdata.port) == str(d.port):
333                    return True
334                else:
335                    d.existing_port     = str(rdata.port)
336                    d.existing_weight = str(rdata.weight)
337
338    if opts.verbose:
339        print("Lookup of %s succeeded, but we failed to find a matching DNS entry for %s" % (normalised_name, d))
340
341    return False
342
343
344def get_subst_vars(samdb):
345    """get the list of substitution vars."""
346    global lp, am_rodc
347    vars = {}
348
349    vars['DNSDOMAIN'] = samdb.domain_dns_name()
350    vars['DNSFOREST'] = samdb.forest_dns_name()
351    vars['HOSTNAME']  = samdb.host_dns_name()
352    vars['NTDSGUID']  = samdb.get_ntds_GUID()
353    vars['SITE']      = samdb.server_site_name()
354    res = samdb.search(base=samdb.get_default_basedn(), scope=SCOPE_BASE, attrs=["objectGUID"])
355    guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0])
356    vars['DOMAINGUID'] = get_string(guid)
357
358    vars['IF_DC'] = ""
359    vars['IF_RWDC'] = "# "
360    vars['IF_RODC'] = "# "
361    vars['IF_PDC'] = "# "
362    vars['IF_GC'] = "# "
363    vars['IF_RWGC'] = "# "
364    vars['IF_ROGC'] = "# "
365    vars['IF_DNS_DOMAIN'] = "# "
366    vars['IF_RWDNS_DOMAIN'] = "# "
367    vars['IF_RODNS_DOMAIN'] = "# "
368    vars['IF_DNS_FOREST'] = "# "
369    vars['IF_RWDNS_FOREST'] = "# "
370    vars['IF_R0DNS_FOREST'] = "# "
371
372    am_rodc = samdb.am_rodc()
373    if am_rodc:
374        vars['IF_RODC'] = ""
375    else:
376        vars['IF_RWDC'] = ""
377
378    if samdb.am_pdc():
379        vars['IF_PDC'] = ""
380
381    # check if we "are DNS server"
382    res = samdb.search(base=samdb.get_config_basedn(),
383                   expression='(objectguid=%s)' % vars['NTDSGUID'],
384                   attrs=["options", "msDS-hasMasterNCs"])
385
386    if len(res) == 1:
387        if "options" in res[0]:
388            options = int(res[0]["options"][0])
389            if (options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
390                vars['IF_GC'] = ""
391                if am_rodc:
392                    vars['IF_ROGC'] = ""
393                else:
394                    vars['IF_RWGC'] = ""
395
396        basedn = str(samdb.get_default_basedn())
397        forestdn = str(samdb.get_root_basedn())
398
399        if "msDS-hasMasterNCs" in res[0]:
400            for e in res[0]["msDS-hasMasterNCs"]:
401                if str(e) == "DC=DomainDnsZones,%s" % basedn:
402                    vars['IF_DNS_DOMAIN'] = ""
403                    if am_rodc:
404                        vars['IF_RODNS_DOMAIN'] = ""
405                    else:
406                        vars['IF_RWDNS_DOMAIN'] = ""
407                if str(e) == "DC=ForestDnsZones,%s" % forestdn:
408                    vars['IF_DNS_FOREST'] = ""
409                    if am_rodc:
410                        vars['IF_RODNS_FOREST'] = ""
411                    else:
412                        vars['IF_RWDNS_FOREST'] = ""
413
414    return vars
415
416
417def call_nsupdate(d, op="add"):
418    """call nsupdate for an entry."""
419    global ccachename, nsupdate_cmd, krb5conf
420
421    assert(op in ["add", "delete"])
422
423    if opts.use_file is not None:
424        if opts.verbose:
425            print("Use File instead of nsupdate for %s (%s)" % (d, op))
426
427        try:
428            rfile = open(opts.use_file, 'r+')
429        except IOError:
430            # Perhaps create it
431            rfile = open(opts.use_file, 'w+')
432            # Open it for reading again, in case someone else got to it first
433            rfile = open(opts.use_file, 'r+')
434        fcntl.lockf(rfile, fcntl.LOCK_EX)
435        (file_dir, file_name) = os.path.split(opts.use_file)
436        (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
437        wfile = os.fdopen(tmp_fd, 'a')
438        rfile.seek(0)
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        os.rename(tmpfile, opts.use_file)
448        fcntl.lockf(rfile, fcntl.LOCK_UN)
449        return
450
451    if opts.verbose:
452        print("Calling nsupdate for %s (%s)" % (d, op))
453
454    normalised_name = d.name.rstrip('.') + '.'
455
456    (tmp_fd, tmpfile) = tempfile.mkstemp()
457    f = os.fdopen(tmp_fd, 'w')
458
459    resolver = get_resolver(d)
460
461    # Local the zone for this name
462    zone = dns.resolver.zone_for_name(normalised_name,
463                                      resolver=resolver)
464
465    # Now find the SOA, or if we can't get a ticket to the SOA,
466    # any server with an NS record we can get a ticket for.
467    #
468    # Thanks to the Kerberos Credentials cache this is not
469    # expensive inside the loop
470    server = get_krb5_rw_dns_server(creds, zone)
471    f.write('server %s\n' % server)
472
473    if d.type == "A":
474        f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
475    if d.type == "AAAA":
476        f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
477    if d.type == "SRV":
478        if op == "add" and d.existing_port is not None:
479            f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
480                                                           d.existing_port, d.dest))
481        f.write("update %s %s %u SRV 0 100 %s %s\n" % (op, normalised_name, default_ttl, d.port, d.dest))
482    if d.type == "CNAME":
483        f.write("update %s %s %u CNAME %s\n" % (op, normalised_name, default_ttl, d.dest))
484    if d.type == "NS":
485        f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
486    if opts.verbose:
487        f.write("show\n")
488    f.write("send\n")
489    f.close()
490
491    # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
492    os.environ["SOCKET_WRAPPER_MTU"] = "2000"
493
494    global error_count
495    if ccachename:
496        os.environ["KRB5CCNAME"] = ccachename
497    try:
498        cmd = nsupdate_cmd[:]
499        cmd.append(tmpfile)
500        env = os.environ
501        if krb5conf:
502            env["KRB5_CONFIG"] = krb5conf
503        if ccachename:
504            env["KRB5CCNAME"] = ccachename
505        ret = subprocess.call(cmd, shell=False, env=env)
506        if ret != 0:
507            if opts.fail_immediately:
508                if opts.verbose:
509                    print("Failed update with %s" % tmpfile)
510                sys.exit(1)
511            error_count = error_count + 1
512            if opts.verbose:
513                print("Failed nsupdate: %d" % ret)
514    except Exception as estr:
515        if opts.fail_immediately:
516            sys.exit(1)
517        error_count = error_count + 1
518        if opts.verbose:
519            print("Failed nsupdate: %s : %s" % (str(d), estr))
520    os.unlink(tmpfile)
521
522    # Let socket_wrapper set the default MTU size
523    os.environ["SOCKET_WRAPPER_MTU"] = "0"
524
525
526def call_samba_tool(d, op="add", zone=None):
527    """call samba-tool dns to update an entry."""
528
529    assert(op in ["add", "delete"])
530
531    if (sub_vars['DNSFOREST'] != sub_vars['DNSDOMAIN']) and \
532       sub_vars['DNSFOREST'].endswith('.' + sub_vars['DNSDOMAIN']):
533        print("Refusing to use samba-tool when forest %s is under domain %s" \
534            % (sub_vars['DNSFOREST'], sub_vars['DNSDOMAIN']))
535
536    if opts.verbose:
537        print("Calling samba-tool dns for %s (%s)" % (d, op))
538
539    normalised_name = d.name.rstrip('.') + '.'
540    if zone is None:
541        if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
542            short_name = '@'
543            zone = sub_vars['DNSDOMAIN']
544        elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
545            short_name = '@'
546            zone = sub_vars['DNSFOREST']
547        elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
548            short_name = '@'
549            zone = '_msdcs.' + sub_vars['DNSFOREST']
550        else:
551            if not normalised_name.endswith('.' + sub_vars['DNSDOMAIN'] + '.'):
552                print("Not Calling samba-tool dns for %s (%s), %s not in %s" % (d, op, normalised_name, sub_vars['DNSDOMAIN'] + '.'))
553                return False
554            elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
555                zone = '_msdcs.' + sub_vars['DNSFOREST']
556            else:
557                zone = sub_vars['DNSDOMAIN']
558            len_zone = len(zone)+2
559            short_name = normalised_name[:-len_zone]
560    else:
561        len_zone = len(zone)+2
562        short_name = normalised_name[:-len_zone]
563
564    if d.type == "A":
565        args = [rpc_server_ip, zone, short_name, "A", d.ip]
566    if d.type == "AAAA":
567        args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
568    if d.type == "SRV":
569        if op == "add" and d.existing_port is not None:
570            print("Not handling modify of existing SRV %s using samba-tool" % d)
571            return False
572            op = "update"
573            args = [rpc_server_ip, zone, short_name, "SRV",
574                    "%s %s %s %s" % (d.existing_weight,
575                                     d.existing_port, "0", "100"),
576                    "%s %s %s %s" % (d.dest, d.port, "0", "100")]
577        else:
578            args = [rpc_server_ip, zone, short_name, "SRV", "%s %s %s %s" % (d.dest, d.port, "0", "100")]
579    if d.type == "CNAME":
580        if d.existing_cname_target is None:
581            args = [rpc_server_ip, zone, short_name, "CNAME", d.dest]
582        else:
583            op = "update"
584            args = [rpc_server_ip, zone, short_name, "CNAME",
585                    d.existing_cname_target.rstrip('.'), d.dest]
586
587    if d.type == "NS":
588        args = [rpc_server_ip, zone, short_name, "NS", d.dest]
589
590    if smb_conf and args:
591        args += ["--configfile=" + smb_conf]
592
593    global error_count
594    try:
595        cmd = cmd_dns()
596        if opts.verbose:
597            print("Calling samba-tool dns %s -k no -P %s" % (op, args))
598        ret = cmd._run("dns", op, "-k", "no", "-P", *args)
599        if ret == -1:
600            if opts.fail_immediately:
601                sys.exit(1)
602            error_count = error_count + 1
603            if opts.verbose:
604                print("Failed 'samba-tool dns' based update of %s" % (str(d)))
605    except Exception as estr:
606        if opts.fail_immediately:
607            sys.exit(1)
608        error_count = error_count + 1
609        if opts.verbose:
610            print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
611        raise
612
613irpc_wb = None
614def cached_irpc_wb(lp):
615    global irpc_wb
616    if irpc_wb is not None:
617        return irpc_wb
618    irpc_wb = winbind.winbind("irpc:winbind_server", lp)
619    return irpc_wb
620
621def rodc_dns_update(d, t, op):
622    '''a single DNS update via the RODC netlogon call'''
623    global sub_vars
624
625    assert(op in ["add", "delete"])
626
627    if opts.verbose:
628        print("Calling netlogon RODC update for %s" % d)
629
630    typemap = {
631        netlogon.NlDnsLdapAtSite       : netlogon.NlDnsInfoTypeNone,
632        netlogon.NlDnsGcAtSite         : netlogon.NlDnsDomainNameAlias,
633        netlogon.NlDnsDsaCname         : netlogon.NlDnsDomainNameAlias,
634        netlogon.NlDnsKdcAtSite        : netlogon.NlDnsInfoTypeNone,
635        netlogon.NlDnsDcAtSite         : netlogon.NlDnsInfoTypeNone,
636        netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
637        netlogon.NlDnsGenericGcAtSite  : netlogon.NlDnsDomainNameAlias
638        }
639
640    w = cached_irpc_wb(lp)
641    dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
642    dns_names.count = 1
643    name = netlogon.NL_DNS_NAME_INFO()
644    name.type = t
645    name.dns_domain_info_type = typemap[t]
646    name.priority = 0
647    name.weight   = 0
648    if d.port is not None:
649        name.port = int(d.port)
650    if op == "add":
651        name.dns_register = True
652    else:
653        name.dns_register = False
654    dns_names.names = [ name ]
655    site_name = text_type(sub_vars['SITE'])
656
657    global error_count
658
659    try:
660        ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
661        if ret_names.names[0].status != 0:
662            print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
663            error_count = error_count + 1
664    except RuntimeError as reason:
665        print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
666        error_count = error_count + 1
667
668    if opts.verbose:
669        print("Called netlogon RODC update for %s" % d)
670
671    if error_count != 0 and opts.fail_immediately:
672        sys.exit(1)
673
674
675def call_rodc_update(d, op="add"):
676    '''RODCs need to use the netlogon API for nsupdate'''
677    global lp, sub_vars
678
679    assert(op in ["add", "delete"])
680
681    # we expect failure for 3268 if we aren't a GC
682    if d.port is not None and int(d.port) == 3268:
683        return
684
685    # map the DNS request to a netlogon update type
686    map = {
687        netlogon.NlDnsLdapAtSite       : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
688        netlogon.NlDnsGcAtSite         : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
689        netlogon.NlDnsDsaCname         : '${NTDSGUID}._msdcs.${DNSFOREST}',
690        netlogon.NlDnsKdcAtSite        : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
691        netlogon.NlDnsDcAtSite         : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
692        netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
693        netlogon.NlDnsGenericGcAtSite  : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
694        }
695
696    for t in map:
697        subname = samba.substitute_var(map[t], sub_vars)
698        if subname.lower() == d.name.lower():
699            # found a match - do the update
700            rodc_dns_update(d, t, op)
701            return
702    if opts.verbose:
703        print("Unable to map to netlogon DNS update: %s" % d)
704
705
706# get the list of DNS entries we should have
707dns_update_list = opts.update_list or lp.private_path('dns_update_list')
708
709dns_update_cache = opts.update_cache or lp.private_path('dns_update_cache')
710
711krb5conf = None
712# only change the krb5.conf if we are not in selftest
713if 'SOCKET_WRAPPER_DIR' not in os.environ:
714    # use our private krb5.conf to avoid problems with the wrong domain
715    # bind9 nsupdate wants the default domain set
716    krb5conf = lp.private_path('krb5.conf')
717    os.environ['KRB5_CONFIG'] = krb5conf
718
719try:
720    file = open(dns_update_list, "r")
721except OSError as e:
722    if opts.update_cache:
723        print("The specified update list does not exist")
724    else:
725        print("The server update list was not found, "
726              "and --update-list was not provided.")
727    print(e)
728    print()
729    parser.print_usage()
730    sys.exit(1)
731
732if opts.nosubs:
733    sub_vars = {}
734else:
735    samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
736
737    # get the substitution dictionary
738    sub_vars = get_subst_vars(samdb)
739
740# build up a list of update commands to pass to nsupdate
741update_list = []
742dns_list = []
743cache_list = []
744delete_list = []
745
746dup_set = set()
747cache_set = set()
748
749rebuild_cache = False
750try:
751    cfile = open(dns_update_cache, 'r+')
752except IOError:
753    # Perhaps create it
754    cfile = open(dns_update_cache, 'w+')
755    # Open it for reading again, in case someone else got to it first
756    cfile = open(dns_update_cache, 'r+')
757fcntl.lockf(cfile, fcntl.LOCK_EX)
758for line in cfile:
759    line = line.strip()
760    if line == '' or line[0] == "#":
761        continue
762    c = parse_dns_line(line, {})
763    if c is None:
764        continue
765    if str(c) not in cache_set:
766        cache_list.append(c)
767        cache_set.add(str(c))
768
769site_specific_rec = []
770
771# read each line, and check that the DNS name exists
772for line in file:
773    line = line.strip()
774
775    if '${SITE}' in line:
776        site_specific_rec.append(line)
777
778    if line == '' or line[0] == "#":
779        continue
780    d = parse_dns_line(line, sub_vars)
781    if d is None:
782        continue
783    if d.type == 'A' and len(IP4s) == 0:
784        continue
785    if d.type == 'AAAA' and len(IP6s) == 0:
786        continue
787    if str(d) not in dup_set:
788        dns_list.append(d)
789        dup_set.add(str(d))
790
791# Perform automatic site coverage by default
792auto_coverage = True
793
794if not am_rodc and auto_coverage:
795    site_names = kcc_utils.uncovered_sites_to_cover(samdb,
796                                                    samdb.server_site_name())
797
798    # Duplicate all site specific records for the uncovered site
799    for site in site_names:
800        to_add = [samba.substitute_var(line, {'SITE': site})
801                  for line in site_specific_rec]
802
803        for site_line in to_add:
804            d = parse_dns_line(site_line,
805                               sub_vars=sub_vars)
806            if d is not None and str(d) not in dup_set:
807                dns_list.append(d)
808                dup_set.add(str(d))
809
810# now expand the entries, if any are A record with ip set to $IP
811# then replace with multiple entries, one for each interface IP
812for d in dns_list:
813    if d.ip != "$IP":
814        continue
815    if d.type == 'A':
816        d.ip = IP4s[0]
817        for i in range(len(IP4s)-1):
818            d2 = dnsobj(str(d))
819            d2.ip = IP4s[i+1]
820            dns_list.append(d2)
821    if d.type == 'AAAA':
822        d.ip = IP6s[0]
823        for i in range(len(IP6s)-1):
824            d2 = dnsobj(str(d))
825            d2.ip = IP6s[i+1]
826            dns_list.append(d2)
827
828# now check if the entries already exist on the DNS server
829for d in dns_list:
830    found = False
831    for c in cache_list:
832        if str(c).lower() == str(d).lower():
833            found = True
834            break
835    if not found:
836        rebuild_cache = True
837        if opts.verbose:
838            print("need cache add: %s" % d)
839    if dns_zone_scavenging:
840        update_list.append(d)
841        if opts.verbose:
842            print("scavenging requires update: %s" % d)
843    elif opts.all_names:
844        update_list.append(d)
845        if opts.verbose:
846            print("force update: %s" % d)
847    elif not check_dns_name(d):
848        update_list.append(d)
849        if opts.verbose:
850            print("need update: %s" % d)
851
852for c in cache_list:
853    found = False
854    for d in dns_list:
855        if str(c).lower() == str(d).lower():
856            found = True
857            break
858    if found:
859        continue
860    rebuild_cache = True
861    if opts.verbose:
862        print("need cache remove: %s" % c)
863    if not opts.all_names and not check_dns_name(c):
864        continue
865    delete_list.append(c)
866    if opts.verbose:
867        print("need delete: %s" % c)
868
869if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
870    if opts.verbose:
871        print("No DNS updates needed")
872    sys.exit(0)
873else:
874    if opts.verbose:
875        print("%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list)))
876
877use_samba_tool = opts.use_samba_tool
878use_nsupdate = opts.use_nsupdate
879# get our krb5 creds
880if delete_list or update_list and not opts.nocreds:
881    try:
882        creds = get_credentials(lp)
883    except RuntimeError as e:
884        ccachename = None
885
886        if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
887            raise
888
889        if use_nsupdate:
890            raise
891
892        print("Failed to get Kerberos credentials, falling back to samba-tool: %s" % e)
893        use_samba_tool = True
894
895
896# ask nsupdate to delete entries as needed
897for d in delete_list:
898    if d.rpc or (not use_nsupdate and use_samba_tool):
899        if opts.verbose:
900            print("update (samba-tool): %s" % d)
901        call_samba_tool(d, op="delete", zone=d.zone)
902
903    elif am_rodc:
904        if d.name.lower() == domain.lower():
905            if opts.verbose:
906                print("skip delete (rodc): %s" % d)
907            continue
908        if not d.type in [ 'A', 'AAAA' ]:
909            if opts.verbose:
910                print("delete (rodc): %s" % d)
911            call_rodc_update(d, op="delete")
912        else:
913            if opts.verbose:
914                print("delete (nsupdate): %s" % d)
915            call_nsupdate(d, op="delete")
916    else:
917        if opts.verbose:
918            print("delete (nsupdate): %s" % d)
919        call_nsupdate(d, op="delete")
920
921# ask nsupdate to add entries as needed
922for d in update_list:
923    if d.rpc or (not use_nsupdate and use_samba_tool):
924        if opts.verbose:
925            print("update (samba-tool): %s" % d)
926        call_samba_tool(d, zone=d.zone)
927
928    elif am_rodc:
929        if d.name.lower() == domain.lower():
930            if opts.verbose:
931                print("skip (rodc): %s" % d)
932            continue
933        if not d.type in [ 'A', 'AAAA' ]:
934            if opts.verbose:
935                print("update (rodc): %s" % d)
936            call_rodc_update(d)
937        else:
938            if opts.verbose:
939                print("update (nsupdate): %s" % d)
940            call_nsupdate(d)
941    else:
942        if opts.verbose:
943            print("update(nsupdate): %s" % d)
944        call_nsupdate(d)
945
946if rebuild_cache:
947    print("Rebuilding cache at %s" % dns_update_cache)
948    (file_dir, file_name) = os.path.split(dns_update_cache)
949    (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
950    wfile = os.fdopen(tmp_fd, 'a')
951    for d in dns_list:
952        if opts.verbose:
953            print("Adding %s to %s" % (str(d), file_name))
954        wfile.write(str(d)+"\n")
955    wfile.flush()
956    os.rename(tmpfile, dns_update_cache)
957fcntl.lockf(cfile, fcntl.LOCK_UN)
958
959# delete the ccache if we created it
960if ccachename is not None:
961    os.unlink(ccachename)
962
963if error_count != 0:
964    print("Failed update of %u entries" % error_count)
965sys.exit(error_count)
966