From 8d19da49c4259411ff333946019f4b981fab2bcf Mon Sep 17 00:00:00 2001 From: David Kupka Date: Wed, 2 Dec 2015 14:20:50 +0000 Subject: [PATCH] dns: Check if domain already exists. Raise an error when the domain already exists. This can be overriden using --force or --allow-zone-overlap options. https://fedorahosted.org/freeipa/ticket/3681 Reviewed-By: Petr Spacek --- install/tools/ipa-dns-install | 3 + install/tools/man/ipa-dns-install.1 | 3 + install/tools/man/ipa-server-install.1 | 3 + ipapython/ipautil.py | 18 ++- ipaserver/install/bindinstance.py | 166 ++++++++++++++++--------- ipaserver/install/dns.py | 36 ++++++ ipaserver/install/server/common.py | 19 +++ 7 files changed, 189 insertions(+), 59 deletions(-) diff --git a/install/tools/ipa-dns-install b/install/tools/ipa-dns-install index bdaffd30b..4fd75670f 100755 --- a/install/tools/ipa-dns-install +++ b/install/tools/ipa-dns-install @@ -57,6 +57,9 @@ def parse_options(): help="The reverse DNS zone to use. This option can be used multiple times") parser.add_option("--no-reverse", dest="no_reverse", action="store_true", default=False, help="Do not create new reverse DNS zone") + parser.add_option("--allow-zone-overlap", dest="allow_zone_overlap", + action="store_true", default=False, help="Create DNS " + "zone even if it already exists") parser.add_option("--no-dnssec-validation", dest="no_dnssec_validation", action="store_true", default=False, help="Disable DNSSEC validation") parser.add_option("--dnssec-master", dest="dnssec_master", action="store_true", diff --git a/install/tools/man/ipa-dns-install.1 b/install/tools/man/ipa-dns-install.1 index 2f2d43db7..5ec51c452 100644 --- a/install/tools/man/ipa-dns-install.1 +++ b/install/tools/man/ipa-dns-install.1 @@ -62,6 +62,9 @@ Copy OpenDNSSEC metadata from the specified kasp.db file. This will not create a \fB\-\-zonemgr\fR The e\-mail address of the DNS zone manager. Defaults to hostmaster@DOMAIN .TP +\fB\-\-allow\-zone\-overlap\fR +Allow creatin of (reverse) zone even if the zone is already resolvable. Using this option is discouraged as it result in later problems with domain name resolution. +.TP \fB\-U\fR, \fB\-\-unattended\fR An unattended installation that will never prompt for user input .SH "DEPRECATED OPTIONS" diff --git a/install/tools/man/ipa-server-install.1 b/install/tools/man/ipa-server-install.1 index 7106e9ed5..98adf1398 100644 --- a/install/tools/man/ipa-server-install.1 +++ b/install/tools/man/ipa-server-install.1 @@ -175,6 +175,9 @@ Do not automatically create DNS SSHFP records. .TP \fB\-\-no\-dnssec\-validation\fR Disable DNSSEC validation on this server. +.TP +\fB\-\-allow\-zone\-overlap\fR +Allow creatin of (reverse) zone even if the zone is already resolvable. Using this option is discouraged as it result in later problems with domain name resolution. .SS "UNINSTALL OPTIONS" .TP diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py index 4cee81e64..7509f08c1 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -40,7 +40,7 @@ from contextlib import contextmanager import locale import collections -from dns import resolver, rdatatype +from dns import resolver, rdatatype, reversename from dns.exception import DNSException, Timeout import six from six.moves import input @@ -1031,6 +1031,22 @@ def host_exists(host): return True +def reverse_record_exists(ip_address): + """ + Checks if IP address have some reverse record somewhere. + Does not care where it points. + + Returns True/False + """ + reverse = reversename.from_address(str(ip_address)) + try: + resolver.query(reverse, "PTR") + except DNSException: + # really don't care what exception, PTR is simply unresolvable + return False + return True + + def check_zone_overlap(zone, raise_on_timeout=True): root_logger.info("Checking DNS domain %s, please wait ..." % zone) if not isinstance(zone, DNSName): diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py index 6bfde83de..f590badb4 100644 --- a/ipaserver/install/bindinstance.py +++ b/ipaserver/install/bindinstance.py @@ -43,10 +43,12 @@ from ipaplatform.constants import constants from ipaplatform.paths import paths from ipaplatform.tasks import tasks from ipalib.util import (validate_zonemgr_str, normalize_zonemgr, - get_dns_forward_zone_update_policy, get_dns_reverse_zone_update_policy, - normalize_zone, get_reverse_zone_default, zone_is_reverse, - validate_dnssec_global_forwarder, DNSSECSignatureMissingError, - EDNS0UnsupportedError, UnresolvableRecordError) + get_dns_forward_zone_update_policy, + get_dns_reverse_zone_update_policy, + normalize_zone, get_reverse_zone_default, + zone_is_reverse, validate_dnssec_global_forwarder, + DNSSECSignatureMissingError, EDNS0UnsupportedError, + UnresolvableRecordError, verify_host_resolvable) from ipalib.constants import CACERT if six.PY3: @@ -278,20 +280,48 @@ def find_reverse_zone(ip_address, api=api): return None -def read_reverse_zone(default, ip_address): +def read_reverse_zone(default, ip_address, allow_zone_overlap=False): while True: zone = ipautil.user_input("Please specify the reverse zone name", default=default) if not zone: return None - if verify_reverse_zone(zone, ip_address): - break - else: - print("Invalid reverse zone %s for IP address %s" % (zone, ip_address)) + if not verify_reverse_zone(zone, ip_address): + root_logger.error("Invalid reverse zone %s for IP address %s" + % (zone, ip_address)) + continue + if not allow_zone_overlap: + try: + ipautil.check_zone_overlap(zone, raise_on_timeout=False) + except ValueError as e: + root_logger.error("Reverse zone %s will not be used: %s" + % (zone, e)) + continue + break return normalize_zone(zone) + +def get_auto_reverse_zones(ip_addresses): + auto_zones = [] + for ip in ip_addresses: + if ipautil.reverse_record_exists(ip): + # PTR exist there is no reason to create reverse zone + root_logger.info("Reverse record for IP address %s already " + "exists" % ip) + continue + default_reverse = get_reverse_zone_default(ip) + try: + ipautil.check_zone_overlap(default_reverse) + except ValueError: + root_logger.info("Reverse zone %s for IP address %s already exists" + % (default_reverse, ip)) + continue + auto_zones.append((ip, default_reverse)) + return auto_zones + def add_zone(name, zonemgr=None, dns_backup=None, ns_hostname=None, - update_policy=None, force=False, api=api): + update_policy=None, force=False, skip_overlap_check=False, + api=api): # always normalize zones name = normalize_zone(name) @@ -317,6 +347,7 @@ def add_zone(name, zonemgr=None, dns_backup=None, ns_hostname=None, idnsupdatepolicy=unicode(update_policy), idnsallowquery=u'any', idnsallowtransfer=u'none', + skip_overlap_check=skip_overlap_check, force=force) except (errors.DuplicateEntry, errors.EmptyModlist): pass @@ -406,46 +437,62 @@ def zonemgr_callback(option, opt_str, value, parser): parser.values.zonemgr = value -def check_reverse_zones(ip_addresses, reverse_zones, options, unattended, search_reverse_zones=False): - reverse_asked = False - ret_reverse_zones = [] - # check that there is IP address in every reverse zone - if reverse_zones: - for rz in reverse_zones: - for ip in ip_addresses: - if verify_reverse_zone(rz, ip): - ret_reverse_zones.append(normalize_zone(rz)) - break - else: - # no ip matching reverse zone found - sys.exit("There is no IP address matching reverse zone %s." % rz) - if not options.no_reverse: - # check that there is reverse zone for every IP - for ip in ip_addresses: - if search_reverse_zones and find_reverse_zone(str(ip)): - # reverse zone is already in LDAP +def check_reverse_zones(ip_addresses, reverse_zones, options, unattended, + search_reverse_zones=False): + checked_reverse_zones = [] + + if not options.no_reverse and not reverse_zones: + if unattended: + options.no_reverse = True + else: + options.no_reverse = not create_reverse() + + # shortcut + if options.no_reverse: + return [] + + # verify zones passed in options + for rz in reverse_zones: + # isn't the zone managed by someone else + if not options.allow_zone_overlap: + try: + ipautil.check_zone_overlap(rz) + except ValueError as e: + msg = "Reverse zone %s will not be used: %s" % (rz, e) + if options.unattended: + sys.exit(msg) + else: + root_logger.warning(msg) continue - for rz in ret_reverse_zones: - if verify_reverse_zone(rz, ip): - # reverse zone was entered by user - break - else: - # no reverse zone for ip found - if not reverse_asked: - if not unattended and not reverse_zones: - # user did not specify reverse_zone nor no_reverse - options.no_reverse = not create_reverse() - if options.no_reverse: - # user decided not to create reverse zone - return [] - reverse_asked = True - rz = get_reverse_zone_default(str(ip)) - if not unattended: - rz = read_reverse_zone(rz, str(ip)) - ret_reverse_zones.append(rz) + checked_reverse_zones.append(normalize_zone(rz)) + + # check that there is reverse zone for every IP + ips_missing_reverse = [] + for ip in ip_addresses: + if search_reverse_zones and find_reverse_zone(str(ip)): + # reverse zone is already in LDAP + continue + for rz in checked_reverse_zones: + if verify_reverse_zone(rz, ip): + # reverse zone was entered by user + break + else: + ips_missing_reverse.append(ip) + + # create reverse zone for IP addresses that does not have one + for (ip, rz) in get_auto_reverse_zones(ips_missing_reverse): + if unattended: + root_logger.warning("Missing reverse record for IP address %s" + % ip) + else: + if ipautil.user_input("Do you want to create reverse zone for IP " + "%s" % ip, True): + rz = read_reverse_zone(rz, str(ip), options.allow_zone_overlap) + checked_reverse_zones.append(rz) + + return checked_reverse_zones - return ret_reverse_zones def check_forwarders(dns_forwarders, logger): print("Checking DNS forwarders, please wait ...") @@ -770,7 +817,8 @@ class BindInstance(service.Service): def __setup_zone(self): # Always use force=True as named is not set up yet add_zone(self.domain, self.zonemgr, dns_backup=self.dns_backup, - ns_hostname=self.api.env.host, force=True, api=self.api) + ns_hostname=self.api.env.host, force=True, + skip_overlap_check=True, api=self.api) add_rr(self.domain, "_kerberos", "TXT", self.realm, api=self.api) @@ -788,7 +836,8 @@ class BindInstance(service.Service): # Always use force=True as named is not set up yet for reverse_zone in self.reverse_zones: add_zone(reverse_zone, self.zonemgr, ns_hostname=self.api.env.host, - dns_backup=self.dns_backup, force=True, api=self.api) + dns_backup=self.dns_backup, force=True, + skip_overlap_check=True, api=self.api) def __add_master_records(self, fqdn, addrs): host, zone = fqdn.split(".", 1) @@ -817,18 +866,19 @@ class BindInstance(service.Service): api=self.api) if not dns_zone_exists(zone, self.api): - # add DNS domain for host first - root_logger.debug( - "Host domain (%s) is different from DNS domain (%s)!" % ( - zone, self.domain)) - root_logger.debug("Add DNS zone for host first.") - - add_zone(zone, self.zonemgr, dns_backup=self.dns_backup, - ns_hostname=self.fqdn, force=True, api=self.api) + # check if master hostname is resolvable + try: + verify_host_resolvable(fqdn, root_logger) + except errors.DNSNotARecordError: + root_logger.warning("Master FQDN (%s) is not resolvable.", + fqdn) # Add forward and reverse records to self for addr in addrs: - add_fwd_rr(zone, host, addr, api=self.api) + try: + add_fwd_rr(zone, host, addr, self.api) + except errors.NotFound as e: + pass reverse_zone = find_reverse_zone(addr, self.api) if reverse_zone: diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py index 258bf5dbe..94e9017b6 100644 --- a/ipaserver/install/dns.py +++ b/ipaserver/install/dns.py @@ -13,11 +13,13 @@ from subprocess import CalledProcessError from ipalib import api from ipalib import errors +from ipalib import util from ipaplatform.paths import paths from ipaplatform.constants import constants from ipaplatform import services from ipapython import ipautil from ipapython import sysrestore +from ipapython import dnsutil from ipapython.dn import DN from ipapython.ipa_log_manager import root_logger from ipapython.ipaldap import AUTOBIND_ENABLED @@ -97,6 +99,19 @@ def _disable_dnssec(): conn.update_entry(entry) +def check_dns_enabled(api): + try: + api.Backend.rpcclient.connect() + result = api.Backend.rpcclient.forward( + 'dns_is_enabled', + version=u'2.112', # All the way back to 3.0 servers + ) + return result['result'] + finally: + if api.Backend.rpcclient.isconnected(): + api.Backend.rpcclient.disconnect() + + def install_check(standalone, replica, options, hostname): global ip_addresses global reverse_zones @@ -106,6 +121,27 @@ def install_check(standalone, replica, options, hostname): raise RuntimeError("Integrated DNS requires '%s' package" % constants.IPA_DNS_PACKAGE_NAME) + # when installing first replica with DNS we need to check zone overlap + if not replica or not check_dns_enabled(api): + domain = dnsutil.DNSName(util.normalize_zone(api.env.domain)) + print("Checking DNS domain %s, please wait ..." % domain) + try: + ipautil.check_zone_overlap(domain, raise_on_timeout=False) + except ValueError as e: + if options.force or options.allow_zone_overlap: + root_logger.warning(e.message) + else: + raise e + + for reverse_zone in options.reverse_zones: + try: + ipautil.check_zone_overlap(reverse_zone) + except ValueError as e: + if options.force or options.allow_zone_overlap: + root_logger.warning(e.message) + else: + raise e + if standalone: print("==============================================================================") print("This program will setup DNS for the FreeIPA Server.") diff --git a/ipaserver/install/server/common.py b/ipaserver/install/server/common.py index 1c161120b..3ea0cdead 100644 --- a/ipaserver/install/server/common.py +++ b/ipaserver/install/server/common.py @@ -10,6 +10,8 @@ from ipapython.install import common, core from ipapython.install.core import Knob from ipalib.util import validate_domain_name from ipaserver.install import bindinstance +from ipapython.ipautil import check_zone_overlap +from ipapython.dnsutil import DNSName VALID_SUBJECT_ATTRS = ['st', 'o', 'ou', 'dnqualifier', 'c', 'serialnumber', 'l', 'title', 'sn', 'givenname', @@ -171,6 +173,11 @@ class BaseServerDNS(common.Installable, core.Group, core.Composite): description="Do not add any DNS forwarders, use root servers instead", ) + allow_zone_overlap = Knob( + bool, False, + description="Create DNS zone even if it already exists", + ) + reverse_zones = Knob( (list, str), [], description=("The reverse DNS zone to use. This option can be used " @@ -179,6 +186,12 @@ class BaseServerDNS(common.Installable, core.Group, core.Composite): cli_metavar='REVERSE_ZONE', ) + @reverse_zones.validator + def reverse_zones(self, values): + if not self.allow_zone_overlap: + for zone in values: + check_zone_overlap(zone) + no_reverse = Knob( bool, False, description="Do not create new reverse DNS zone", @@ -255,6 +268,11 @@ class BaseServer(common.Installable, common.Interactive, core.Composite): @domain_name.validator def domain_name(self, value): validate_domain_name(value) + if (self.setup_dns and + not self.dns.allow_zone_overlap): # pylint: disable=no-member + print("Checking DNS domain %s, please wait ..." % value) + check_zone_overlap(value, False) + dm_password = Knob( str, None, @@ -452,6 +470,7 @@ class BaseServer(common.Installable, common.Interactive, core.Composite): self.no_forwarders = self.dns.no_forwarders self.reverse_zones = self.dns.reverse_zones self.no_reverse = self.dns.no_reverse + self.allow_zone_overlap = self.dns.allow_zone_overlap self.no_dnssec_validation = self.dns.no_dnssec_validation self.dnssec_master = self.dns.dnssec_master self.disable_dnssec_master = self.dns.disable_dnssec_master