diff options
author | Martin Kosek <mkosek@redhat.com> | 2012-05-11 14:38:09 +0200 |
---|---|---|
committer | Martin Kosek <mkosek@redhat.com> | 2012-05-24 13:55:56 +0200 |
commit | f1ed123caddd7525a0081c4a9de931cabdfda43f (patch) | |
tree | f615dabc3535203fbd2777166dbe150f6d31197e | |
parent | 6bb462e26a814e683b3ec5b39d2ff9a1db8fa4ec (diff) | |
download | freeipa-f1ed123caddd7525a0081c4a9de931cabdfda43f.tar.gz freeipa-f1ed123caddd7525a0081c4a9de931cabdfda43f.tar.xz freeipa-f1ed123caddd7525a0081c4a9de931cabdfda43f.zip |
Replace DNS client based on acutil with python-dns
IPA client and server tool set used authconfig acutil module to
for client DNS operations. This is not optimal DNS interface for
several reasons:
- does not provide native Python object oriented interface
but but rather C-like interface based on functions and
structures which is not easy to use and extend
- acutil is not meant to be used by third parties besides
authconfig and thus can break without notice
Replace the acutil with python-dns package which has a feature rich
interface for dealing with all different aspects of DNS including
DNSSEC. The main target of this patch is to replace all uses of
acutil DNS library with a use python-dns. In most cases, even
though the larger parts of the code are changed, the actual
functionality is changed only in the following cases:
- redundant DNS checks were removed from verify_fqdn function
in installutils to make the whole DNS check simpler and
less error-prone. Logging was improves for the remaining
checks
- improved logging for ipa-client-install DNS discovery
https://fedorahosted.org/freeipa/ticket/2730
https://fedorahosted.org/freeipa/ticket/1837
-rw-r--r-- | freeipa.spec.in | 8 | ||||
-rwxr-xr-x | install/tools/ipa-dns-install | 2 | ||||
-rwxr-xr-x | ipa-client/ipa-install/ipa-client-install | 24 | ||||
-rw-r--r-- | ipa-client/ipaclient/ipadiscovery.py | 142 | ||||
-rw-r--r-- | ipa-client/ipaclient/ntpconf.py | 14 | ||||
-rw-r--r-- | ipalib/plugins/dns.py | 14 | ||||
-rw-r--r-- | ipalib/rpc.py | 21 | ||||
-rw-r--r-- | ipalib/util.py | 16 | ||||
-rw-r--r-- | ipapython/README | 3 | ||||
-rw-r--r-- | ipapython/config.py | 64 | ||||
-rw-r--r-- | ipapython/dnsclient.py | 469 | ||||
-rw-r--r-- | ipapython/ipautil.py | 24 | ||||
-rw-r--r-- | ipaserver/install/installutils.py | 120 |
13 files changed, 200 insertions, 721 deletions
diff --git a/freeipa.spec.in b/freeipa.spec.in index a1e43a0ad..de93aecb6 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -59,7 +59,6 @@ BuildRequires: m4 BuildRequires: libtool BuildRequires: gettext BuildRequires: python-devel -BuildRequires: authconfig BuildRequires: python-ldap BuildRequires: python-setuptools BuildRequires: python-krbV @@ -79,6 +78,7 @@ BuildRequires: python-memcached BuildRequires: sssd >= 1.8.0 BuildRequires: python-lxml BuildRequires: python-pyasn1 >= 0.0.9a +BuildRequires: python-dns %description IPA is an integrated solution to provide centrally managed Identity (machine, @@ -151,6 +151,7 @@ Requires(postun): python systemd-units Requires(preun): python initscripts chkconfig Requires(postun): python initscripts chkconfig %endif +Requires: python-dns # We have a soft-requires on bind. It is an optional part of # IPA but if it is configured we need a way to require versions @@ -220,6 +221,7 @@ Requires: nss-tools Requires: bind-utils Requires: oddjob-mkhomedir Requires: python-krbV +Requires: python-dns Obsoletes: ipa-client >= 1.0 @@ -256,7 +258,6 @@ Group: System Environment/Libraries %if 0%{?fedora} >= 12 || 0%{?rhel} >= 6 Requires: python-kerberos >= 1.1-3 %endif -Requires: authconfig Requires: gnupg Requires: iproute Requires: pyOpenSSL @@ -683,6 +684,9 @@ fi %ghost %attr(0644,root,apache) %config(noreplace) %{_sysconfdir}/ipa/ca.crt %changelog +* Fri May 11 2012 Martin Kosek <mkosek@redhat.com> - 2.99.0-29 +- Replace used DNS client library (acutil) with python-dns + * Tue Apr 10 2012 Rob Crittenden <rcritten@redhat.com> - 2.99.0-28 - Set min for selinux-policy to 3.10.0-110 on F-17 to pick up certmonger policy for restarting services. diff --git a/install/tools/ipa-dns-install b/install/tools/ipa-dns-install index b540630f4..10547c342 100755 --- a/install/tools/ipa-dns-install +++ b/install/tools/ipa-dns-install @@ -223,7 +223,7 @@ def main(): zone_notif=options.zone_notif) bind.create_instance() - # Restart http instance to make sure acutil has the right resolver + # Restart http instance to make sure that python-dns has the right resolver # https://bugzilla.redhat.com/show_bug.cgi?id=800368 http = httpinstance.HTTPInstance(fstore) service.print_msg("Restarting the web server") diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install index 67279b3ed..6854581d2 100755 --- a/ipa-client/ipa-install/ipa-client-install +++ b/ipa-client/ipa-install/ipa-client-install @@ -25,6 +25,7 @@ try: import os import time import socket + from ipapython.ipa_log_manager import * import tempfile import getpass @@ -35,7 +36,6 @@ try: from ipapython.ipautil import run, user_input, CalledProcessError, file_exists, realm_to_suffix import ipapython.services as ipaservices from ipapython import ipautil - from ipapython import dnsclient from ipapython import sysrestore from ipapython import version from ipapython import certmonger @@ -996,18 +996,10 @@ def update_dns(server, hostname): def client_dns(server, hostname, dns_updates=False): - dns_ok = False + dns_ok = ipautil.is_host_resolvable(hostname) - # Check if the client has an A record registered in its name. - rs = dnsclient.query(hostname+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_A) - if len([ rec for rec in rs if rec.dns_type is not dnsclient.DNS_T_SOA ]) > 0: - dns_ok = True - else: - rs = dnsclient.query(hostname+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_AAAA) - if len([ rec for rec in rs if rec.dns_type is not dnsclient.DNS_T_SOA ]) > 0: - dns_ok = True - else: - print "Warning: Hostname (%s) not found in DNS" % hostname + if not dns_ok: + print "Warning: Hostname (%s) not found in DNS" % hostname if dns_updates or not dns_ok: update_dns(server, hostname) @@ -1243,15 +1235,15 @@ def install(options, env, fstore, statestore): # We assume that NTP servers are discoverable through SRV records in the DNS # If that fails, we try to sync directly with IPA server, assuming it runs NTP print 'Synchronizing time with KDC...' - ntp_servers = ipautil.parse_items(ds.ipadnssearchntp(cli_domain)) + ntp_servers = ds.ipadns_search_srv(cli_domain, '_ntp._udp', None, break_on_first=False) synced_ntp = False - if len(ntp_servers) > 0: + if ntp_servers: for s in ntp_servers: - synced_ntp = ipaclient.ntpconf.synconce_ntp(s) + synced_ntp = ipaclient.ntpconf.synconce_ntp(s, debug=True) if synced_ntp: break if not synced_ntp: - synced_ntp = ipaclient.ntpconf.synconce_ntp(cli_server) + synced_ntp = ipaclient.ntpconf.synconce_ntp(cli_server, debug=True) if not synced_ntp: print "Unable to sync time with IPA NTP server, assuming the time is in sync." (krb_fd, krb_name) = tempfile.mkstemp() diff --git a/ipa-client/ipaclient/ipadiscovery.py b/ipa-client/ipaclient/ipadiscovery.py index 86bef28b2..281daf42a 100644 --- a/ipa-client/ipaclient/ipadiscovery.py +++ b/ipa-client/ipaclient/ipadiscovery.py @@ -20,12 +20,14 @@ import socket import os from ipapython.ipa_log_manager import * -import ipapython.dnsclient import tempfile import ldap from ldap import LDAPError +from dns import resolver, rdatatype +from dns.exception import DNSException + from ipapython.ipautil import run, CalledProcessError, valid_ip, get_ipa_basedn, \ - realm_to_suffix, format_netloc, parse_items + realm_to_suffix, format_netloc NOT_FQDN = -1 @@ -93,11 +95,12 @@ class IPADiscovery: isn't found. """ server = None + root_logger.debug("Start searching for LDAP SRV record in %s and" + " its sub-domains", domain) while not server: - root_logger.debug("[ipadnssearchldap("+domain+")]") - server = self.ipadnssearchldap(domain) + server = self.ipadns_search_srv(domain, '_ldap._tcp', 389) if server: - return (server, domain) + return (server[0], domain) else: p = domain.find(".") if p == -1: #no ldap server found and last component of the domain already tested @@ -148,11 +151,13 @@ class IPADiscovery: if not self.domain: #no ldap server found return NO_LDAP_SERVER else: - root_logger.debug("[ipadnssearchldap]") - self.server = self.ipadnssearchldap(domain) - if self.server: + root_logger.debug("Search for LDAP SRV record in %s", domain) + server = self.ipadns_search_srv(domain, '_ldap._tcp', 389) + if server: + self.server = server[0] self.domain = domain else: + self.server = None return NO_LDAP_SERVER else: #server forced on us, this means DNS doesn't work :/ @@ -172,19 +177,16 @@ class IPADiscovery: root_logger.debug("[ipacheckldap]") # We may have received multiple servers corresponding to the domain # Iterate through all of those to check if it is IPA LDAP server - servers = parse_items(self.server) ldapret = [NOT_IPA_SERVER] ldapaccess = True - for server in servers: + if self.server: # check ldap now - ldapret = self.ipacheckldap(server, self.realm) + ldapret = self.ipacheckldap(self.server, self.realm) if ldapret[0] == 0: self.server = ldapret[1] self.realm = ldapret[2] - break - - if ldapret[0] == NO_ACCESS_TO_LDAP: + elif ldapret[0] == NO_ACCESS_TO_LDAP: ldapaccess = False # If one of LDAP servers checked rejects access (may be anonymous @@ -310,46 +312,43 @@ class IPADiscovery: os.rmdir(temp_ca_dir) - def ipadnssearchldap(self, tdomain): - servers = "" - rserver = "" + def ipadns_search_srv(self, domain, srv_record_name, default_port, + break_on_first=True): + """ + Search for SRV records in given domain. When no record is found, + en empty string is returned + + :param domain: Search domain name + :param srv_record_name: SRV record name, e.g. "_ldap._tcp" + :param default_port: When default_port is not None, it is being + checked with the port in SRV record and if they don't + match, the port from SRV record is appended to + found hostname in this format: "hostname:port" + :param break_on_first: break on the first find and return just one + entry + """ + servers = [] - qname = "_ldap._tcp."+tdomain - # terminate the name - if not qname.endswith("."): - qname += "." - results = ipapython.dnsclient.query(qname, ipapython.dnsclient.DNS_C_IN, ipapython.dnsclient.DNS_T_SRV) + qname = '%s.%s' % (srv_record_name, domain) - for result in results: - if result.dns_type == ipapython.dnsclient.DNS_T_SRV: - rserver = result.rdata.server.rstrip(".") - if result.rdata.port and result.rdata.port != 389: - rserver += ":" + str(result.rdata.port) - if servers: - servers += "," + rserver - else: - servers = rserver - break + root_logger.debug("Search DNS for SRV record of %s", qname) - return servers - - def ipadnssearchntp(self, tdomain): - servers = "" - rserver = "" - - qname = "_ntp._udp."+tdomain - # terminate the name - if not qname.endswith("."): - qname += "." - results = ipapython.dnsclient.query(qname, ipapython.dnsclient.DNS_C_IN, ipapython.dnsclient.DNS_T_SRV) - - for result in results: - if result.dns_type == ipapython.dnsclient.DNS_T_SRV: - rserver = result.rdata.server.rstrip(".") - if servers: - servers += "," + rserver - else: - servers = rserver + try: + answers = resolver.query(qname, rdatatype.SRV) + except DNSException, e: + root_logger.debug("DNS record not found: %s", e.__class__.__name__) + answers = [] + + for answer in answers: + root_logger.debug("DNS record found: %s", answer) + server = str(answer.target).rstrip(".") + if not server: + root_logger.debug("Cannot parse the hostname from SRV record: %s", answer) + continue + if default_port is not None and answer.port != default_port: + server = "%s:%s" % (server, str(answer.port)) + servers.append(server) + if break_on_first: break return servers @@ -359,35 +358,32 @@ class IPADiscovery: kdc = None # now, check for a Kerberos realm the local host or domain is in qname = "_kerberos." + tdomain - # terminate the name - if not qname.endswith("."): - qname += "." - results = ipapython.dnsclient.query(qname, ipapython.dnsclient.DNS_C_IN, ipapython.dnsclient.DNS_T_TXT) - - for result in results: - if result.dns_type == ipapython.dnsclient.DNS_T_TXT: - realm = result.rdata.data + + root_logger.debug("Search DNS for TXT record of %s", qname) + + try: + answers = resolver.query(qname, rdatatype.TXT) + except DNSException, e: + root_logger.debug("DNS record not found: %s", e.__class__.__name__) + answers = [] + + for answer in answers: + root_logger.debug("DNS record found: %s", answer) + if answer.strings: + realm = answer.strings[0] if realm: break if realm: # now fetch server information for the realm - qname = "_kerberos._udp." + realm.lower() - # terminate the name - if not qname.endswith("."): - qname += "." - results = ipapython.dnsclient.query(qname, ipapython.dnsclient.DNS_C_IN, ipapython.dnsclient.DNS_T_SRV) - for result in results: - if result.dns_type == ipapython.dnsclient.DNS_T_SRV: - qname = result.rdata.server.rstrip(".") - if result.rdata.port and result.rdata.port != 88: - qname += ":" + str(result.rdata.port) - if kdc: - kdc += "," + qname - else: - kdc = qname + domain = realm.lower() + + kdc = self.ipadns_search_srv(domain, '_kerberos._udp', 88, + break_on_first=False) if not kdc: root_logger.debug("SRV record for KDC not found! Realm: %s, SRV record: %s" % (realm, qname)) + kdc = None + kdc = ','.join(kdc) return [realm, kdc] diff --git a/ipa-client/ipaclient/ntpconf.py b/ipa-client/ipaclient/ntpconf.py index e71692f40..aa9261cb2 100644 --- a/ipa-client/ipaclient/ntpconf.py +++ b/ipa-client/ipaclient/ntpconf.py @@ -133,7 +133,7 @@ def config_ntp(server_fqdn, fstore = None, sysstore = None): # Restart ntpd ipaservices.knownservices.ntpd.restart() -def synconce_ntp(server_fqdn): +def synconce_ntp(server_fqdn, debug=False): """ Syncs time with specified server using ntpdate. Primarily designed to be used before Kerberos setup @@ -142,15 +142,17 @@ def synconce_ntp(server_fqdn): Returns True if sync was successful """ ntpdate="/usr/sbin/ntpdate" - result = False if os.path.exists(ntpdate): # retry several times -- logic follows /etc/init.d/ntpdate # implementation + cmd = [ntpdate, "-U", "ntp", "-s", "-b"] + if debug: + cmd.append('-d') + cmd.append(server_fqdn) for retry in range(0,3): try: - ipautil.run([ntpdate, "-U", "ntp", "-s", "-b", server_fqdn]) - result = True - break + ipautil.run(cmd) + return True except: pass - return result + return False diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index b0e65ab94..e26332d46 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -30,8 +30,7 @@ from ipalib.plugins.baseldap import * from ipalib import _, ngettext from ipalib.util import (validate_zonemgr, normalize_zonemgr, validate_hostname, validate_dns_label, validate_domain_name) -from ipapython import dnsclient -from ipapython.ipautil import valid_ip, CheckedIPAddress +from ipapython.ipautil import valid_ip, CheckedIPAddress, is_host_resolvable from ldap import explode_dn __doc__ = _(""" @@ -2610,17 +2609,8 @@ class dns_resolve(Command): query = '%s.%s.' % (query, api.env.domain) if query[-1] != '.': query = query + '.' - reca = dnsclient.query(query, dnsclient.DNS_C_IN, dnsclient.DNS_T_A) - rec6 = dnsclient.query(query, dnsclient.DNS_C_IN, dnsclient.DNS_T_AAAA) - records = reca + rec6 - found = False - for rec in records: - if rec.dns_type == dnsclient.DNS_T_A or \ - rec.dns_type == dnsclient.DNS_T_AAAA: - found = True - break - if not found: + if not is_host_resolvable(query): raise errors.NotFound( reason=_('Host \'%(host)s\' not found') % {'host': query} ) diff --git a/ipalib/rpc.py b/ipalib/rpc.py index 04a3f3e35..bd18b6bbf 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -39,11 +39,15 @@ import errno import locale from xmlrpclib import Binary, Fault, dumps, loads, ServerProxy, Transport, ProtocolError import kerberos +from dns import resolver, rdatatype +from dns.exception import DNSException + from ipalib.backend import Connectible from ipalib.errors import public_errors, PublicError, UnknownError, NetworkError, KerberosError, XMLRPCMarshallError from ipalib import errors from ipalib.request import context, Connection -from ipapython import ipautil, dnsclient +from ipapython import ipautil + import httplib import socket from ipapython.nsslib import NSSHTTPS, NSSConnection @@ -349,11 +353,16 @@ class xmlclient(Connectible): (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(self.env.xmlrpc_uri) servers = [] name = '_ldap._tcp.%s.' % self.env.domain - rs = dnsclient.query(name, dnsclient.DNS_C_IN, dnsclient.DNS_T_SRV) - for r in rs: - if r.dns_type == dnsclient.DNS_T_SRV: - rsrv = r.rdata.server.rstrip('.') - servers.append('https://%s%s' % (ipautil.format_netloc(rsrv), path)) + + try: + answers = resolver.query(name, rdatatype.SRV) + except DNSException, e: + answers = [] + + for answer in answers: + server = str(answer.target).rstrip(".") + servers.append('https://%s%s' % (ipautil.format_netloc(server), path)) + servers = list(set(servers)) # the list/set conversion won't preserve order so stick in the # local config file version here. diff --git a/ipalib/util.py b/ipalib/util.py index 64ac6b2cf..50da74327 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -28,11 +28,12 @@ import socket import re from types import NoneType from weakref import WeakKeyDictionary +from dns import resolver, rdatatype +from dns.exception import DNSException from ipalib import errors from ipalib.text import _ from ipalib.dn import DN, RDN -from ipapython import dnsclient from ipapython.ipautil import decode_ssh_pubkey @@ -88,16 +89,17 @@ def validate_host_dns(log, fqdn): """ See if the hostname has a DNS A record. """ - rs = dnsclient.query(fqdn + '.', dnsclient.DNS_C_IN, dnsclient.DNS_T_A) - if len(rs) == 0: + try: + answers = resolver.query(fqdn, rdatatype.A) log.debug( - 'IPA: DNS A record lookup failed for %s' % fqdn + 'IPA: found %d records for %s: %s' % (len(answers), fqdn, + ' '.join(str(answer) for answer in answers)) ) - raise errors.DNSNotARecordError() - else: + except DNSException, e: log.debug( - 'IPA: found %d records for %s' % (len(rs), fqdn) + 'IPA: DNS A record lookup failed for %s' % fqdn ) + raise errors.DNSNotARecordError() def isvalid_base64(data): """ diff --git a/ipapython/README b/ipapython/README index ec2bb3a52..a724a7faa 100644 --- a/ipapython/README +++ b/ipapython/README @@ -3,10 +3,9 @@ geared currently towards command-line tools. A brief overview: -config.py - identify the IPA server domain and realm. It uses dnsclient to +config.py - identify the IPA server domain and realm. It uses python-dns to try to detect this information first and will fall back to /etc/ipa/default.conf if that fails. -dnsclient.py - find IPA information via DNS ipautil.py - helper functions diff --git a/ipapython/config.py b/ipapython/config.py index d4c724dc9..d428b1e27 100644 --- a/ipapython/config.py +++ b/ipapython/config.py @@ -20,9 +20,11 @@ import ConfigParser from optparse import Option, Values, OptionParser, IndentedHelpFormatter, OptionValueError from copy import copy +from dns import resolver, rdatatype +from dns.exception import DNSException +import dns.name import socket -import ipapython.dnsclient import re import urlparse @@ -163,7 +165,7 @@ def __parse_config(discover_server = True): pass def __discover_config(discover_server = True): - rl = 0 + servers = [] try: if not config.default_realm: try: @@ -177,34 +179,44 @@ def __discover_config(discover_server = True): return False if not config.default_domain: - #try once with REALM -> domain - dom_name = str(config.default_realm).lower() - name = "_ldap._tcp."+dom_name+"." - rs = ipapython.dnsclient.query(name, ipapython.dnsclient.DNS_C_IN, ipapython.dnsclient.DNS_T_SRV) - rl = len(rs) - if rl == 0: - #try cycling on domain components of FQDN - dom_name = socket.getfqdn() - while rl == 0: - tok = dom_name.find(".") - if tok == -1: + # try once with REALM -> domain + domain = str(config.default_realm).lower() + name = "_ldap._tcp." + domain + + try: + servers = resolver.query(name, rdatatype.SRV) + except DNSException: + # try cycling on domain components of FQDN + try: + domain = dns.name.from_text(socket.getfqdn()) + except DNSException: return False - dom_name = dom_name[tok+1:] - name = "_ldap._tcp." + dom_name + "." - rs = ipapython.dnsclient.query(name, ipapython.dnsclient.DNS_C_IN, ipapython.dnsclient.DNS_T_SRV) - rl = len(rs) - config.default_domain = dom_name + while True: + domain = domain.parent() + + if str(domain) == '.': + return False + name = "_ldap._tcp.%s" % domain + try: + servers = resolver.query(name, rdatatype.SRV) + break + except DNSException: + pass + + config.default_domain = str(domain).rstrip(".") if discover_server: - if rl == 0: - name = "_ldap._tcp."+config.default_domain+"." - rs = ipapython.dnsclient.query(name, ipapython.dnsclient.DNS_C_IN, ipapython.dnsclient.DNS_T_SRV) - - for r in rs: - if r.dns_type == ipapython.dnsclient.DNS_T_SRV: - rsrv = r.rdata.server.rstrip(".") - config.default_server.append(rsrv) + if not servers: + name = "_ldap._tcp.%s." % config.default_domain + try: + servers = resolver.query(name, rdatatype.SRV) + except DNSException: + pass + + for server in servers: + hostname = str(server.target).rstrip(".") + config.default_server.append(hostname) except: pass diff --git a/ipapython/dnsclient.py b/ipapython/dnsclient.py deleted file mode 100644 index 3f08866a6..000000000 --- a/ipapython/dnsclient.py +++ /dev/null @@ -1,469 +0,0 @@ -# -# Copyright 2001, 2005 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import struct -import socket -import sys - -import acutil - -DNS_C_IN = 1 -DNS_C_CS = 2 -DNS_C_CHAOS = 3 -DNS_C_HS = 4 -DNS_C_ANY = 255 - -DNS_T_A = 1 -DNS_T_NS = 2 -DNS_T_CNAME = 5 -DNS_T_SOA = 6 -DNS_T_NULL = 10 -DNS_T_WKS = 11 -DNS_T_PTR = 12 -DNS_T_HINFO = 13 -DNS_T_MX = 15 -DNS_T_TXT = 16 -DNS_T_AAAA = 28 -DNS_T_SRV = 33 -DNS_T_ANY = 255 - -DNS_S_QUERY = 1 -DNS_S_ANSWER = 2 -DNS_S_AUTHORITY = 3 -DNS_S_ADDITIONAL = 4 - -DEBUG_DNSCLIENT = False - -class DNSQueryHeader: - FORMAT = "!HBBHHHH" - def __init__(self): - self.dns_id = 0 - self.dns_rd = 0 - self.dns_tc = 0 - self.dns_aa = 0 - self.dns_opcode = 0 - self.dns_qr = 0 - self.dns_rcode = 0 - self.dns_z = 0 - self.dns_ra = 0 - self.dns_qdcount = 0 - self.dns_ancount = 0 - self.dns_nscount = 0 - self.dns_arcount = 0 - - def pack(self): - return struct.pack(DNSQueryHeader.FORMAT, - self.dns_id, - (self.dns_rd & 1) | - (self.dns_tc & 1) << 1 | - (self.dns_aa & 1) << 2 | - (self.dns_opcode & 15) << 3 | - (self.dns_qr & 1) << 7, - (self.dns_rcode & 15) | - (self.dns_z & 7) << 4 | - (self.dns_ra & 1) << 7, - self.dns_qdcount, - self.dns_ancount, - self.dns_nscount, - self.dns_arcount) - - def unpack(self, data): - (self.dns_id, byte1, byte2, self.dns_qdcount, self.dns_ancount, - self.dns_nscount, self.dns_arcount) = struct.unpack(DNSQueryHeader.FORMAT, data[0:self.size()]) - self.dns_rd = byte1 & 1 - self.dns_tc = (byte1 >> 1) & 1 - self.dns_aa = (byte1 >> 2) & 1 - self.dns_opcode = (byte1 >> 3) & 15 - self.dns_qr = (byte1 >> 7) & 1 - self.dns_rcode = byte2 & 15 - self.dns_z = (byte2 >> 4) & 7 - self.dns_ra = (byte1 >> 7) & 1 - - def size(self): - return struct.calcsize(DNSQueryHeader.FORMAT) - -def unpackQueryHeader(data): - header = DNSQueryHeader() - header.unpack(data) - return header - -class DNSResult: - FORMAT = "!HHIH" - QFORMAT = "!HH" - def __init__(self): - self.dns_name = "" - self.dns_type = 0 - self.dns_class = 0 - self.dns_ttl = 0 - self.dns_rlength = 0 - self.rdata = None - self.section = None - - def unpack(self, data): - (self.dns_type, self.dns_class, self.dns_ttl, - self.dns_rlength) = struct.unpack(DNSResult.FORMAT, data[0:self.size()]) - - def qunpack(self, data): - (self.dns_type, self.dns_class) = struct.unpack(DNSResult.QFORMAT, data[0:self.qsize()]) - - def size(self): - return struct.calcsize(DNSResult.FORMAT) - - def qsize(self): - return struct.calcsize(DNSResult.QFORMAT) - -class DNSRData: - def __init__(self): - pass - -#typedef struct dns_rr_a { -# u_int32_t address; -#} dns_rr_a_t; -# -#typedef struct dns_rr_aaaa { -# unsigned char address[16]; -#} dns_rr_aaaa_t; -# -#typedef struct dns_rr_cname { -# const char *cname; -#} dns_rr_cname_t; -# -#typedef struct dns_rr_hinfo { -# const char *cpu, *os; -#} dns_rr_hinfo_t; -# -#typedef struct dns_rr_mx { -# u_int16_t preference; -# const char *exchange; -#} dns_rr_mx_t; -# -#typedef struct dns_rr_null { -# unsigned const char *data; -#} dns_rr_null_t; -# -#typedef struct dns_rr_ns { -# const char *nsdname; -#} dns_rr_ns_t; -# -#typedef struct dns_rr_ptr { -# const char *ptrdname; -#} dns_rr_ptr_t; -# -#typedef struct dns_rr_soa { -# const char *mname; -# const char *rname; -# u_int32_t serial; -# int32_t refresh; -# int32_t retry; -# int32_t expire; -# int32_t minimum; -#} dns_rr_soa_t; -# -#typedef struct dns_rr_txt { -# const char *data; -#} dns_rr_txt_t; -# -#typedef struct dns_rr_srv { -# const char *server; -# u_int16_t priority; -# u_int16_t weight; -# u_int16_t port; -#} dns_rr_srv_t; - -def dnsNameToLabel(name): - out = "" - name = name.split(".") - for part in name: - out += chr(len(part)) + part - return out - -def dnsFormatQuery(query, qclass, qtype): - header = DNSQueryHeader() - - header.dns_id = 0 # FIXME: id = 0 - header.dns_rd = 1 # don't know why the original code didn't request recursion for non SOA requests - header.dns_qr = 0 # query - header.dns_opcode = 0 # standard query - header.dns_qdcount = 1 # single query - - qlabel = dnsNameToLabel(query) - if not qlabel: - return "" - - out = header.pack() + qlabel - out += chr(qtype >> 8) - out += chr(qtype & 0xff) - out += chr(qclass >> 8) - out += chr(qclass & 0xff) - - return out - -def dnsParseLabel(label, base): - # returns (output, rest) - if not label: - return ("", None) - - update = 1 - rest = label - output = "" - skip = 0 - - try: - while ord(rest[0]): - if ord(rest[0]) & 0xc0: - rest = base[((ord(rest[0]) & 0x3f) << 8) + ord(rest[1]):] - if update: - skip += 2 - update = 0 - continue - output += rest[1:ord(rest[0]) + 1] + "." - if update: - skip += ord(rest[0]) + 1 - rest = rest[ord(rest[0]) + 1:] - except IndexError: - return ("", None) - return (label[skip+update:], output) - -def dnsParseA(data, base): - rdata = DNSRData() - if len(data) < 4: - rdata.address = 0 - return None - - rdata.address = (ord(data[0])<<24) | (ord(data[1])<<16) | (ord(data[2])<<8) | (ord(data[3])<<0) - - if DEBUG_DNSCLIENT: - print "A = %d.%d.%d.%d." % (ord(data[0]), ord(data[1]), ord(data[2]), ord(data[3])) - return rdata - -def dnsParseAAAA(data, base): - rdata = DNSRData() - if len(data) < 16: - rdata.address = 0 - return None - - rdata.address = list(struct.unpack('!16B', data)) - if DEBUG_DNSCLIENT: - print socket.inet_ntop(socket.AF_INET6, - struct.pack('!16B', *rdata.address)) - return rdata - -def dnsParseText(data): - if len(data) < 1: - return ("", None) - tlen = ord(data[0]) - if len(data) < tlen + 1: - return ("", None) - return (data[tlen+1:], data[1:tlen+1]) - -def dnsParseNS(data, base): - rdata = DNSRData() - (rest, rdata.nsdname) = dnsParseLabel(data, base) - if DEBUG_DNSCLIENT: - print "NS DNAME = \"%s\"." % (rdata.nsdname) - return rdata - -def dnsParseCNAME(data, base): - rdata = DNSRData() - (rest, rdata.cname) = dnsParseLabel(data, base) - if DEBUG_DNSCLIENT: - print "CNAME = \"%s\"." % (rdata.cname) - return rdata - -def dnsParseSOA(data, base): - rdata = DNSRData() - format = "!IIIII" - - (rest, rdata.mname) = dnsParseLabel(data, base) - if rdata.mname is None: - return None - (rest, rdata.rname) = dnsParseLabel(rest, base) - if rdata.rname is None: - return None - if len(rest) < struct.calcsize(format): - return None - - (rdata.serial, rdata.refresh, rdata.retry, rdata.expire, - rdata.minimum) = struct.unpack(format, rest[:struct.calcsize(format)]) - - if DEBUG_DNSCLIENT: - print "SOA(mname) = \"%s\"." % rdata.mname - print "SOA(rname) = \"%s\"." % rdata.rname - print "SOA(serial) = %d." % rdata.serial - print "SOA(refresh) = %d." % rdata.refresh - print "SOA(retry) = %d." % rdata.retry - print "SOA(expire) = %d." % rdata.expire - print "SOA(minimum) = %d." % rdata.minimum - return rdata - -def dnsParseNULL(data, base): - # um, yeah - return None - -def dnsParseWKS(data, base): - return None - -def dnsParseHINFO(data, base): - rdata = DNSRData() - (rest, rdata.cpu) = dnsParseText(data) - if rest: - (rest, rdata.os) = dnsParseText(rest) - if DEBUG_DNSCLIENT: - print "HINFO(cpu) = \"%s\"." % rdata.cpu - print "HINFO(os) = \"%s\"." % rdata.os - return rdata - -def dnsParseMX(data, base): - rdata = DNSRData() - if len(data) < 2: - return None - rdata.preference = (ord(data[0]) << 8) | ord(data[1]) - (rest, rdata.exchange) = dnsParseLabel(data[2:], base) - if DEBUG_DNSCLIENT: - print "MX(exchanger) = \"%s\"." % rdata.exchange - print "MX(preference) = %d." % rdata.preference - return rdata - -def dnsParseTXT(data, base): - rdata = DNSRData() - (rest, rdata.data) = dnsParseText(data) - if DEBUG_DNSCLIENT: - print "TXT = \"%s\"." % rdata.data - return rdata - -def dnsParsePTR(data, base): - rdata = DNSRData() - (rest, rdata.ptrdname) = dnsParseLabel(data, base) - if DEBUG_DNSCLIENT: - print "PTR = \"%s\"." % rdata.ptrdname - return rdata - -def dnsParseSRV(data, base): - rdata = DNSRData() - format = "!HHH" - flen = struct.calcsize(format) - if len(data) < flen: - return None - - (rdata.priority, rdata.weight, rdata.port) = struct.unpack(format, data[:flen]) - (rest, rdata.server) = dnsParseLabel(data[flen:], base) - if DEBUG_DNSCLIENT: - print "SRV(server) = \"%s\"." % rdata.server - print "SRV(weight) = %d." % rdata.weight - print "SRV(priority) = %d." % rdata.priority - print "SRV(port) = %d." % rdata.port - return rdata - -def dnsParseResults(results): - try: - header = unpackQueryHeader(results) - except struct.error: - return [] - - if header.dns_qr != 1: # should be a response - return [] - - if header.dns_rcode != 0: # should be no error - return [] - - rest = results[header.size():] - - rrlist = [] - - for i in xrange(header.dns_qdcount): - if not rest: - return [] - - qq = DNSResult() - - (rest, label) = dnsParseLabel(rest, results) - if label is None: - return [] - - if len(rest) < qq.qsize(): - return [] - - qq.qunpack(rest) - - rest = rest[qq.qsize():] - - if DEBUG_DNSCLIENT: - print "Queried for '%s', class = %d, type = %d." % (label, - qq.dns_class, qq.dns_type) - - for (rec_count, section_id) in ((header.dns_ancount, DNS_S_ANSWER), - (header.dns_nscount, DNS_S_AUTHORITY), - (header.dns_arcount, DNS_S_ADDITIONAL)): - for i in xrange(rec_count): - (rest, label) = dnsParseLabel(rest, results) - if label is None: - return [] - - rr = DNSResult() - - rr.dns_name = label - rr.section = section_id - - if len(rest) < rr.size(): - return [] - - rr.unpack(rest) - - rest = rest[rr.size():] - - if DEBUG_DNSCLIENT: - print "Answer %d for '%s', class = %d, type = %d, ttl = %d." % (i, - rr.dns_name, rr.dns_class, rr.dns_type, - rr.dns_ttl) - - if len(rest) < rr.dns_rlength: - if DEBUG_DNSCLIENT: - print "Answer too short." - return [] - - fmap = { DNS_T_A: dnsParseA, DNS_T_NS: dnsParseNS, - DNS_T_CNAME: dnsParseCNAME, DNS_T_SOA: dnsParseSOA, - DNS_T_NULL: dnsParseNULL, DNS_T_WKS: dnsParseWKS, - DNS_T_PTR: dnsParsePTR, DNS_T_HINFO: dnsParseHINFO, - DNS_T_MX: dnsParseMX, DNS_T_TXT: dnsParseTXT, - DNS_T_AAAA : dnsParseAAAA, DNS_T_SRV: dnsParseSRV} - - if not rr.dns_type in fmap: - if DEBUG_DNSCLIENT: - print "Don't know how to parse RR type %d!" % rr.dns_type - else: - rr.rdata = fmap[rr.dns_type](rest[:rr.dns_rlength], results) - - rest = rest[rr.dns_rlength:] - rrlist += [rr] - - return rrlist - -def query(query, qclass, qtype): - qdata = dnsFormatQuery(query, qclass, qtype) - if not qdata: - return [] - answer = acutil.res_send(qdata) - if not answer: - return [] - return dnsParseResults(answer) - -if __name__ == '__main__': - DEBUG_DNSCLIENT = True - print "Sending query." - rr = query(len(sys.argv) > 1 and sys.argv[1] or "devserv.devel.redhat.com.", - DNS_C_IN, DNS_T_ANY) - sys.exit(0) diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py index 4a9db11e2..8884e7be9 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -41,6 +41,8 @@ import re import xmlrpclib import datetime import netaddr +from dns import resolver, rdatatype +from dns.exception import DNSException from ipapython.ipa_log_manager import * from ipapython import ipavalidate @@ -611,17 +613,6 @@ def ipa_generate_password(characters=None,pwd_len=None): rndpwd += rndchar return rndpwd -def parse_items(text): - '''Given text with items separated by whitespace or comma, return a list of those items - - The returned list only contains non-empty items. - ''' - split_re = re.compile('[ ,\t\n]+') - items = split_re.split(text) - for item in items[:]: - if not item: items.remove(item) - return items - def user_input(prompt, default = None, allow_empty = True): if default == None: while True: @@ -747,6 +738,17 @@ def bind_port_responder(port, socket_type=socket.SOCK_STREAM, socket_timeout=Non finally: s.close() +def is_host_resolvable(fqdn): + for rdtype in (rdatatype.A, rdatatype.AAAA): + try: + resolver.query(fqdn, rdtype) + except DNSException: + continue + else: + return True + + return False + def get_ipa_basedn(conn): """ Get base DN of IPA suffix in given LDAP server. diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index fc54b74b3..362723614 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -30,9 +30,11 @@ import netaddr import time import tempfile import shutil -from ConfigParser import SafeConfigParser +from dns import resolver, rdatatype +from dns.exception import DNSException -from ipapython import ipautil, dnsclient, sysrestore +from ConfigParser import SafeConfigParser +from ipapython import ipautil, sysrestore from ipapython.ipa_log_manager import * from ipalib.util import validate_hostname @@ -76,68 +78,6 @@ def get_fqdn(): fqdn = "" return fqdn -def verify_dns_records(host_name, responses, resaddr, family): - familykw = { 'ipv4' : { - 'dns_type' : dnsclient.DNS_T_A, - 'socket_family' : socket.AF_INET, - }, - 'ipv6' : { - 'dns_type' : dnsclient.DNS_T_AAAA, - 'socket_family' : socket.AF_INET6, - }, - } - - family = family.lower() - if family not in familykw.keys(): - raise RuntimeError("Unknown faimily %s\n" % family) - - rec_list = [] - for rsn in responses: - if rsn.section == dnsclient.DNS_S_ANSWER and \ - rsn.dns_type == familykw[family]['dns_type']: - rec_list.append(rsn) - - if not rec_list: - raise IOError(errno.ENOENT, - "Warning: Hostname (%s) not found in DNS" % host_name) - - if family == 'ipv4': - familykw[family]['address'] = [socket.inet_ntop(socket.AF_INET, - struct.pack('!L',rec.rdata.address)) \ - for rec in rec_list] - else: - familykw[family]['address'] = [socket.inet_ntop(socket.AF_INET6, - struct.pack('!16B', *rec.rdata.address)) \ - for rec in rec_list] - - # Check that DNS address is the same is address returned via standard glibc calls - dns_addrs = [netaddr.IPAddress(addr) for addr in familykw[family]['address']] - dns_addr = None - for addr in dns_addrs: - if addr.format() == resaddr: - dns_addr = addr - break - - if dns_addr is None: - raise RuntimeError("Host address %s does not match any address in DNS lookup." % resaddr) - - rs = dnsclient.query(dns_addr.reverse_dns, dnsclient.DNS_C_IN, dnsclient.DNS_T_PTR) - if len(rs) == 0: - raise RuntimeError("Cannot find Reverse Address for %s (%s)" % (host_name, dns_addr.format())) - - rev = None - for rsn in rs: - if rsn.dns_type == dnsclient.DNS_T_PTR: - rev = rsn - break - - if rev == None: - raise RuntimeError("Cannot find Reverse Address for %s (%s)" % (host_name, dns_addr.format())) - - if rec.dns_name != rev.rdata.ptrdname: - raise RuntimeError("The DNS forward record %s does not match the reverse address %s" % (rec.dns_name, rev.rdata.ptrdname)) - - def verify_fqdn(host_name, no_host_dns=False, local_hostname=True): """ Run fqdn checks for given host: @@ -168,7 +108,9 @@ def verify_fqdn(host_name, no_host_dns=False, local_hostname=True): if local_hostname: try: + root_logger.debug('Check if %s is a primary hostname for localhost', host_name) ex_name = socket.gethostbyaddr(host_name) + root_logger.debug('Primary hostname for localhost: %s', ex_name[0]) if host_name != ex_name[0]: raise HostLookupError("The host name %s does not match the primary host name %s. "\ "Please check /etc/hosts or DNS name resolution" % (host_name, ex_name[0])) @@ -180,43 +122,41 @@ def verify_fqdn(host_name, no_host_dns=False, local_hostname=True): return try: + root_logger.debug('Search DNS for %s', host_name) hostaddr = socket.getaddrinfo(host_name, None) - except: + except Exception, e: + root_logger.debug('Search failed: %s', e) raise HostForwardLookupError("Unable to resolve host name, check /etc/hosts or DNS name resolution") if len(hostaddr) == 0: raise HostForwardLookupError("Unable to resolve host name, check /etc/hosts or DNS name resolution") + # Verify this is NOT a CNAME + try: + root_logger.debug('Check if %s is not a CNAME', host_name) + resolver.query(host_name, rdatatype.CNAME) + raise HostReverseLookupError("The IPA Server Hostname cannot be a CNAME, only A and AAAA names are allowed.") + except DNSException: + pass + + # list of verified addresses to prevent multiple searches for the same address + verified = set() for a in hostaddr: - if a[4][0] == '127.0.0.1' or a[4][0] == '::1': - raise HostForwardLookupError("The IPA Server hostname must not resolve to localhost (%s). A routable IP address must be used. Check /etc/hosts to see if %s is an alias for %s" % (a[4][0], host_name, a[4][0])) + address = a[4][0] + if address in verified: + continue + if address == '127.0.0.1' or address == '::1': + raise HostForwardLookupError("The IPA Server hostname must not resolve to localhost (%s). A routable IP address must be used. Check /etc/hosts to see if %s is an alias for %s" % (address, host_name, address)) try: - resaddr = a[4][0] - revname = socket.gethostbyaddr(a[4][0])[0] - except: + root_logger.debug('Check reverse address of %s', address) + revname = socket.gethostbyaddr(address)[0] + except Exception, e: + root_logger.debug('Check failed: %s', e) raise HostReverseLookupError("Unable to resolve the reverse ip address, check /etc/hosts or DNS name resolution") + root_logger.debug('Found reverse name: %s', revname) if revname != host_name: raise HostReverseLookupError("The host name %s does not match the reverse lookup %s" % (host_name, revname)) - - # Verify this is NOT a CNAME - rs = dnsclient.query(host_name+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_CNAME) - if len(rs) != 0: - for rsn in rs: - if rsn.dns_type == dnsclient.DNS_T_CNAME: - raise HostReverseLookupError("The IPA Server Hostname cannot be a CNAME, only A and AAAA names are allowed.") - - # Verify that it is a DNS A or AAAA record - rs = dnsclient.query(host_name+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_A) - if len([ rec for rec in rs if rec.dns_type is not dnsclient.DNS_T_SOA ]) > 0: - verify_dns_records(host_name, rs, resaddr, 'ipv4') - return - - rs = dnsclient.query(host_name+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_AAAA) - if len([ rec for rec in rs if rec.dns_type is not dnsclient.DNS_T_SOA ]) > 0: - verify_dns_records(host_name, rs, resaddr, 'ipv6') - return - else: - print "Warning: Hostname (%s) not found in DNS" % host_name + verified.add(address) def record_in_hosts(ip, host_name=None, file="/etc/hosts"): """ |