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