2013-09-28 17:17:12 +02:00
|
|
|
"""
|
2013-09-28 13:47:49 +02:00
|
|
|
Misc. DNS related code: query, dynamic update, etc.
|
2013-09-29 17:21:05 +02:00
|
|
|
|
|
|
|
Usually, higher level code wants to call the add/update/delete functions.
|
2013-09-28 13:47:49 +02:00
|
|
|
"""
|
|
|
|
|
2013-10-27 05:14:47 +01:00
|
|
|
# time to wait for dns name resolving [s]
|
|
|
|
RESOLVER_TIMEOUT = 5.0
|
|
|
|
|
|
|
|
# time to wait for dns name updating [s]
|
|
|
|
UPDATE_TIMEOUT = 20.0
|
|
|
|
|
|
|
|
# time after we retry to reach a previously unreachable ns [s]
|
|
|
|
UNAVAILABLE_RETRY = 300.0
|
|
|
|
|
|
|
|
|
2013-11-14 02:11:44 +01:00
|
|
|
import time
|
2013-10-27 05:14:47 +01:00
|
|
|
from datetime import timedelta
|
|
|
|
|
2013-10-18 15:30:17 -07:00
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2013-11-27 07:14:39 +01:00
|
|
|
import socket
|
|
|
|
|
2013-09-28 17:02:43 +02:00
|
|
|
import dns.inet
|
2013-09-28 15:29:16 +02:00
|
|
|
import dns.name
|
|
|
|
import dns.resolver
|
|
|
|
import dns.query
|
|
|
|
import dns.update
|
|
|
|
import dns.tsig
|
|
|
|
import dns.tsigkeyring
|
|
|
|
|
2013-10-27 05:14:47 +01:00
|
|
|
from django.utils.timezone import now
|
2013-10-03 19:26:39 +02:00
|
|
|
|
2013-09-28 15:29:16 +02:00
|
|
|
|
2013-11-01 22:26:06 +01:00
|
|
|
Timeout = dns.resolver.Timeout
|
|
|
|
|
|
|
|
|
2013-09-28 17:02:43 +02:00
|
|
|
class SameIpError(ValueError):
|
|
|
|
"""
|
|
|
|
raised if an IP address is already present in DNS and and update was
|
|
|
|
requested, but is not needed.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2013-11-10 06:29:33 +01:00
|
|
|
class DnsUpdateError(ValueError):
|
|
|
|
"""
|
|
|
|
raised if DNS update return code is not NOERROR
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2013-10-27 05:14:47 +01:00
|
|
|
class NameServerNotAvailable(Exception):
|
|
|
|
"""
|
|
|
|
raised if some nameserver was flagged as not available,
|
|
|
|
but we tried using it.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2013-10-03 20:39:55 +02:00
|
|
|
def check_ip(ipaddr, keys=('ipv4', 'ipv6')):
|
2013-09-29 17:21:05 +02:00
|
|
|
"""
|
2013-10-03 20:39:55 +02:00
|
|
|
Check if a string is a valid ip address and also
|
|
|
|
determine the kind of the address (address family).
|
|
|
|
Return first key for v4, second key for v6.
|
|
|
|
|
|
|
|
:param ipaddr: ip address, v4 or v6, str
|
|
|
|
:param keys: 2-tuple (v4key, v6key)
|
|
|
|
:return: v4key or v6key
|
|
|
|
:raises: ValueError if the ip is invalid
|
2013-09-29 17:21:05 +02:00
|
|
|
"""
|
|
|
|
af = dns.inet.af_for_address(ipaddr)
|
2013-10-03 20:39:55 +02:00
|
|
|
return keys[af == dns.inet.AF_INET6]
|
2013-09-29 17:21:05 +02:00
|
|
|
|
|
|
|
|
2013-10-08 15:39:45 +02:00
|
|
|
def add(fqdn, ipaddr, ttl=60, origin=None):
|
2013-09-29 17:21:05 +02:00
|
|
|
"""
|
|
|
|
intelligent dns adder - first does a lookup on the master server to find
|
|
|
|
the current ip and only sends an 'add' if there is no such entry.
|
|
|
|
otherwise send an 'upd' if the if we have a different ip.
|
|
|
|
|
|
|
|
:param fqdn: fully qualified domain name (str)
|
|
|
|
:param ipaddr: new ip address
|
|
|
|
:param ttl: time to live, default 60s (int)
|
2013-10-08 15:39:45 +02:00
|
|
|
:param origin: origin zone (optional, str)
|
2013-09-29 17:21:05 +02:00
|
|
|
:raises: SameIpError if new and old IP is the same
|
2013-10-03 20:47:25 +02:00
|
|
|
:raises: ValueError if ipaddr is no valid ip address string
|
2013-09-29 17:21:05 +02:00
|
|
|
"""
|
2013-10-03 20:39:55 +02:00
|
|
|
rdtype = check_ip(ipaddr, keys=('A', 'AAAA'))
|
2013-09-29 17:21:05 +02:00
|
|
|
try:
|
2013-10-08 15:39:45 +02:00
|
|
|
current_ipaddr = query_ns(fqdn, rdtype, origin=origin)
|
2013-09-29 17:21:05 +02:00
|
|
|
# check if ip really changed
|
|
|
|
ok = ipaddr != current_ipaddr
|
|
|
|
action = 'upd'
|
|
|
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
|
|
|
# no dns entry yet, ok
|
|
|
|
ok = True
|
|
|
|
action = 'add'
|
|
|
|
if ok:
|
|
|
|
# only send an add/update if the ip really changed as the update
|
|
|
|
# causes write I/O on the nameserver and also traffic to the
|
|
|
|
# dns slaves (they get a notify if we update the zone).
|
2013-10-08 15:39:45 +02:00
|
|
|
update_ns(fqdn, rdtype, ipaddr, action=action, ttl=ttl, origin=origin)
|
2013-09-29 17:21:05 +02:00
|
|
|
else:
|
|
|
|
raise SameIpError
|
|
|
|
|
|
|
|
|
2013-10-08 15:39:45 +02:00
|
|
|
def delete(fqdn, rdtype=None, origin=None):
|
2013-09-29 17:21:05 +02:00
|
|
|
"""
|
|
|
|
dns deleter
|
|
|
|
|
|
|
|
:param fqdn: fully qualified domain name (str)
|
|
|
|
:param rdtype: 'A', 'AAAA' or None (deletes 'A' and 'AAAA')
|
2013-10-08 15:39:45 +02:00
|
|
|
:param origin: origin zone (optional, str)
|
2013-09-29 17:21:05 +02:00
|
|
|
"""
|
|
|
|
if rdtype is not None:
|
|
|
|
assert rdtype in ['A', 'AAAA', ]
|
|
|
|
rdtypes = [rdtype, ]
|
|
|
|
else:
|
|
|
|
rdtypes = ['A', 'AAAA']
|
|
|
|
for rdtype in rdtypes:
|
2013-10-08 15:39:45 +02:00
|
|
|
update_ns(fqdn, rdtype, action='del', origin=origin)
|
2013-09-29 17:21:05 +02:00
|
|
|
|
|
|
|
|
2013-10-08 15:39:45 +02:00
|
|
|
def update(fqdn, ipaddr, ttl=60, origin=None):
|
2013-09-29 02:00:15 +02:00
|
|
|
"""
|
|
|
|
intelligent dns updater - first does a lookup on the master server to find
|
|
|
|
the current ip and only sends a dynamic update if we have a different ip.
|
|
|
|
|
|
|
|
:param fqdn: fully qualified domain name (str)
|
|
|
|
:param ipaddr: new ip address
|
|
|
|
:param ttl: time to live, default 60s (int)
|
2013-10-08 15:39:45 +02:00
|
|
|
:param origin: origin zone (optional, str)
|
2013-09-29 02:00:15 +02:00
|
|
|
:raises: SameIpError if new and old IP is the same
|
2013-10-03 20:47:25 +02:00
|
|
|
:raises: ValueError if ipaddr is no valid ip address string
|
2013-09-29 02:00:15 +02:00
|
|
|
"""
|
2013-10-03 20:39:55 +02:00
|
|
|
rdtype = check_ip(ipaddr, keys=('A', 'AAAA'))
|
2013-09-28 17:02:43 +02:00
|
|
|
try:
|
2013-10-08 15:39:45 +02:00
|
|
|
current_ipaddr = query_ns(fqdn, rdtype, origin=origin)
|
2013-09-28 17:02:43 +02:00
|
|
|
# check if ip really changed
|
|
|
|
ok = ipaddr != current_ipaddr
|
2013-09-29 00:34:26 +02:00
|
|
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
2013-09-28 17:02:43 +02:00
|
|
|
# no dns entry yet, ok
|
|
|
|
ok = True
|
|
|
|
if ok:
|
|
|
|
# only send an update if the ip really changed as the update
|
|
|
|
# causes write I/O on the nameserver and also traffic to the
|
|
|
|
# dns slaves (they get a notify if we update the zone).
|
2013-10-08 15:39:45 +02:00
|
|
|
update_ns(fqdn, rdtype, ipaddr, action='upd', ttl=ttl, origin=origin)
|
2013-09-28 17:02:43 +02:00
|
|
|
else:
|
|
|
|
raise SameIpError
|
|
|
|
|
|
|
|
|
2013-09-29 23:43:41 +02:00
|
|
|
def query_ns(qname, rdtype, origin=None):
|
2013-09-28 13:47:49 +02:00
|
|
|
"""
|
|
|
|
query a dns name from our master server
|
|
|
|
|
|
|
|
:param qname: the query name
|
|
|
|
:type qname: dns.name.Name object or str
|
|
|
|
:param rdtype: the query type
|
|
|
|
:type rdtype: int or str
|
2013-10-03 17:59:55 +02:00
|
|
|
:param origin: origin zone
|
|
|
|
:type origin: str or None
|
2013-10-27 05:14:47 +01:00
|
|
|
:return: IP (as str) or "-" if ns is not available
|
2013-09-28 13:47:49 +02:00
|
|
|
"""
|
2013-09-29 23:43:41 +02:00
|
|
|
origin, name = parse_name(qname, origin)
|
2013-10-27 08:40:54 +01:00
|
|
|
fqdn = name + origin
|
|
|
|
assert fqdn.is_absolute()
|
2013-11-10 05:52:41 +01:00
|
|
|
nameserver, origin = get_ns_info(fqdn)[0:2]
|
2013-09-28 13:47:49 +02:00
|
|
|
resolver = dns.resolver.Resolver(configure=False)
|
|
|
|
# we do not configure it from resolv.conf, but patch in the values we
|
|
|
|
# want into the documented attributes:
|
2013-09-29 23:43:41 +02:00
|
|
|
resolver.nameservers = [nameserver, ]
|
2013-10-27 08:40:54 +01:00
|
|
|
resolver.search = []
|
2013-10-27 05:14:47 +01:00
|
|
|
resolver.lifetime = RESOLVER_TIMEOUT
|
|
|
|
try:
|
2013-10-27 08:40:54 +01:00
|
|
|
answer = resolver.query(fqdn, rdtype)
|
2013-10-27 05:14:47 +01:00
|
|
|
ip = str(list(answer)[0])
|
2013-10-27 08:40:54 +01:00
|
|
|
logger.debug("query: %s answer: %s" % (fqdn.to_text(), ip))
|
2013-10-27 05:14:47 +01:00
|
|
|
return ip
|
|
|
|
except (dns.resolver.Timeout, dns.resolver.NoNameservers): # socket.error also?
|
|
|
|
logger.warning("timeout when querying for name '%s' in zone '%s' with rdtype '%s'." % (
|
|
|
|
name, origin, rdtype))
|
|
|
|
set_ns_availability(origin, False)
|
|
|
|
raise
|
2013-09-28 15:29:16 +02:00
|
|
|
|
|
|
|
|
2013-11-27 07:14:39 +01:00
|
|
|
def rev_lookup(ipaddr):
|
|
|
|
"""
|
|
|
|
do a normal reverse DNS lookup, IP to name
|
|
|
|
|
2013-11-27 08:16:49 +01:00
|
|
|
note: this call may be slow, especially if there is no reverse dns entry
|
|
|
|
|
2013-11-27 07:14:39 +01:00
|
|
|
:param ipaddr: ip address (str)
|
2013-11-27 08:16:49 +01:00
|
|
|
:return: hostname (or empty string if lookup failed)
|
2013-11-27 07:14:39 +01:00
|
|
|
"""
|
2013-11-27 08:16:49 +01:00
|
|
|
name = ''
|
|
|
|
if ipaddr:
|
|
|
|
try:
|
|
|
|
name = socket.gethostbyaddr(ipaddr)[0]
|
|
|
|
except socket.error:
|
|
|
|
pass
|
|
|
|
return name
|
2013-11-27 07:14:39 +01:00
|
|
|
|
|
|
|
|
2013-09-28 15:29:16 +02:00
|
|
|
def parse_name(fqdn, origin=None):
|
|
|
|
"""
|
|
|
|
Parse a fully qualified domain name into a relative name
|
|
|
|
and a origin zone. Please note that the origin return value will
|
|
|
|
have a trailing dot.
|
|
|
|
|
|
|
|
:param fqdn: fully qualified domain name (str)
|
|
|
|
:param origin: origin zone (optional, str)
|
|
|
|
:return: origin, relative name (both dns.name.Name)
|
|
|
|
"""
|
|
|
|
fqdn = dns.name.from_text(fqdn)
|
|
|
|
if origin is None:
|
|
|
|
origin = dns.resolver.zone_for_name(fqdn)
|
|
|
|
rel_name = fqdn.relativize(origin)
|
|
|
|
else:
|
|
|
|
origin = dns.name.from_text(origin)
|
|
|
|
rel_name = fqdn - origin
|
|
|
|
return origin, rel_name
|
|
|
|
|
|
|
|
|
2013-11-10 05:52:41 +01:00
|
|
|
def get_ns_info(fqdn, origin=None):
|
2013-09-29 23:43:41 +02:00
|
|
|
"""
|
2013-11-24 05:04:07 +01:00
|
|
|
Get the master nameserver for the <origin> zone, the key secret needed
|
2013-09-29 23:43:41 +02:00
|
|
|
to update the zone and the key algorithm used.
|
|
|
|
|
2013-11-10 05:52:41 +01:00
|
|
|
:param fqdn: the fully qualified hostname we are dealing with (str)
|
|
|
|
:param origin: zone we are dealing with, must be with trailing dot (default:autodetect) (str)
|
2013-11-24 05:04:07 +01:00
|
|
|
:return: master nameserver, origin, domain, update keyname, update secret, update algo
|
2013-10-27 05:14:47 +01:00
|
|
|
:raises: NameServerNotAvailable if ns was flagged unavailable in the db
|
2013-09-29 23:43:41 +02:00
|
|
|
"""
|
2013-11-10 05:52:41 +01:00
|
|
|
fqdn_str = str(fqdn)
|
|
|
|
origin, name = parse_name(fqdn_str, origin)
|
|
|
|
origin_str = str(origin)
|
2013-10-18 15:30:17 -07:00
|
|
|
from .models import Domain
|
2013-11-10 05:52:41 +01:00
|
|
|
try:
|
|
|
|
# first we check if we have an entry for the fqdn
|
|
|
|
# single-host update secret use case
|
|
|
|
# XXX we need 2 DB accesses for the usual case just to support this rare case
|
|
|
|
domain = fqdn_str.rstrip('.')
|
|
|
|
d = Domain.objects.get(domain=domain)
|
|
|
|
keyname = fqdn_str
|
|
|
|
except Domain.DoesNotExist:
|
|
|
|
# now check the base zone, the usual case
|
|
|
|
# zone update secret use case
|
|
|
|
domain = origin_str.rstrip('.')
|
|
|
|
d = Domain.objects.get(domain=domain)
|
|
|
|
keyname = origin_str
|
2013-10-27 05:14:47 +01:00
|
|
|
if not d.available:
|
|
|
|
if d.last_update + timedelta(seconds=UNAVAILABLE_RETRY) > now():
|
|
|
|
# if there are troubles with a nameserver, we set available=False
|
|
|
|
# and stop trying working with that nameserver for a while
|
|
|
|
raise NameServerNotAvailable("nameserver for domain %s at IP %s was flagged unavailable" % (
|
|
|
|
domain, d.nameserver_ip, ))
|
|
|
|
else:
|
|
|
|
# retry timeout is over, set it available again
|
2013-11-10 05:52:41 +01:00
|
|
|
set_ns_availability(domain, True)
|
2013-10-18 15:30:17 -07:00
|
|
|
algorithm = getattr(dns.tsig, d.nameserver_update_algorithm)
|
2013-11-24 05:04:07 +01:00
|
|
|
return d.nameserver_ip, origin, domain, name, keyname, d.nameserver_update_secret, algorithm
|
2013-09-29 23:43:41 +02:00
|
|
|
|
|
|
|
|
2013-11-10 07:04:46 +01:00
|
|
|
def update_ns(fqdn, rdtype='A', ipaddr=None, origin=None, action='upd', ttl=60):
|
2013-09-28 15:29:16 +02:00
|
|
|
"""
|
|
|
|
update our master server
|
|
|
|
|
2013-09-28 15:36:27 +02:00
|
|
|
:param fqdn: the fully qualified domain name to update (str)
|
|
|
|
:param rdtype: the record type (default: 'A') (str)
|
|
|
|
:param ipaddr: ip address (v4 or v6), if needed (str)
|
|
|
|
:param origin: the origin zone to update (default; autodetect) (str)
|
2013-09-28 15:29:16 +02:00
|
|
|
:param action: 'add', 'del' or 'upd'
|
2013-09-28 15:36:27 +02:00
|
|
|
:param ttl: time to live for the added/updated resource, default 60s (int)
|
|
|
|
:return: dns response
|
2013-09-28 15:29:16 +02:00
|
|
|
"""
|
|
|
|
assert action in ['add', 'del', 'upd', ]
|
2013-11-10 05:52:41 +01:00
|
|
|
nameserver, origin, domain, name, keyname, key, algo = get_ns_info(fqdn, origin)
|
2013-09-28 15:29:16 +02:00
|
|
|
upd = dns.update.Update(origin,
|
2013-11-10 05:52:41 +01:00
|
|
|
keyring=dns.tsigkeyring.from_text({keyname: key}),
|
2013-09-29 23:43:41 +02:00
|
|
|
keyalgorithm=algo)
|
2013-09-28 15:29:16 +02:00
|
|
|
if action == 'add':
|
|
|
|
assert ipaddr is not None
|
|
|
|
upd.add(name, ttl, rdtype, ipaddr)
|
|
|
|
elif action == 'del':
|
|
|
|
upd.delete(name, rdtype)
|
|
|
|
elif action == 'upd':
|
|
|
|
assert ipaddr is not None
|
|
|
|
upd.replace(name, ttl, rdtype, ipaddr)
|
2013-10-18 15:30:17 -07:00
|
|
|
logger.debug("performing %s for name %s and origin %s with rdtype %s and ipaddr %s" % (
|
|
|
|
action, name, origin, rdtype, ipaddr))
|
2013-10-27 05:14:47 +01:00
|
|
|
try:
|
|
|
|
response = dns.query.tcp(upd, nameserver, timeout=UPDATE_TIMEOUT)
|
2013-11-10 06:29:33 +01:00
|
|
|
rcode = response.rcode()
|
|
|
|
if rcode != dns.rcode.NOERROR:
|
|
|
|
rcode_text = dns.rcode.to_text(rcode)
|
|
|
|
logger.warning("DNS error [%s] performing %s for name %s and origin %s with rdtype %s and ipaddr %s" % (
|
|
|
|
rcode_text, action, name, origin, rdtype, ipaddr))
|
|
|
|
raise DnsUpdateError(rcode_text)
|
2013-10-27 05:14:47 +01:00
|
|
|
return response
|
|
|
|
except dns.exception.Timeout:
|
|
|
|
logger.warning("timeout when performing %s for name %s and origin %s with rdtype %s and ipaddr %s" % (
|
|
|
|
action, name, origin, rdtype, ipaddr))
|
2013-11-10 05:52:41 +01:00
|
|
|
set_ns_availability(domain, False)
|
2013-10-27 05:14:47 +01:00
|
|
|
raise
|
2013-11-01 07:40:42 +01:00
|
|
|
except dns.tsig.PeerBadSignature:
|
2013-11-10 05:52:41 +01:00
|
|
|
logger.error("PeerBadSignature - shared secret mismatch? zone: %s" % (origin, ))
|
2013-11-10 07:04:46 +01:00
|
|
|
set_ns_availability(domain, False)
|
|
|
|
raise DnsUpdateError("PeerBadSignature")
|
2013-10-27 05:14:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
def set_ns_availability(domain, available):
|
|
|
|
"""
|
|
|
|
Set availability of the master nameserver for <domain>.
|
|
|
|
|
|
|
|
As each Timeout takes quite a while, we want to avoid it.
|
|
|
|
|
|
|
|
:param domain: domain object or string, may end with "."
|
|
|
|
:param available: True/False for availability of ns
|
|
|
|
"""
|
|
|
|
from .models import Domain
|
|
|
|
domain = str(domain).rstrip('.')
|
|
|
|
d = Domain.objects.get(domain=domain)
|
|
|
|
d.available = available
|
|
|
|
d.save()
|
|
|
|
if available:
|
|
|
|
logger.info("set zone '%s' to available" % domain)
|
|
|
|
else:
|
|
|
|
logger.warning("set zone '%s' to unavailable" % domain)
|
2013-11-14 02:11:44 +01:00
|
|
|
|
|
|
|
|
|
|
|
def put_ip_into_session(session, ipaddr, kind=None, max_age=0,
|
|
|
|
save=False):
|
|
|
|
"""
|
|
|
|
put an IP address into the session, including a timestamp,
|
|
|
|
so we know how fresh it is.
|
|
|
|
|
|
|
|
:param session: the session object
|
|
|
|
:param ipaddr: ip address (can be v4 or v6, str)
|
|
|
|
:param kind: 'ipv4' or 'ipv6' or None to autodetect
|
|
|
|
:param max_age: maximum age of info, if older we refresh the timestamp
|
|
|
|
:param save: save the session, if it was changed
|
|
|
|
"""
|
|
|
|
if kind is None:
|
|
|
|
kind = check_ip(ipaddr)
|
|
|
|
# we try to avoid modifying the session if not necessary...
|
|
|
|
if session.get(kind) != ipaddr:
|
|
|
|
# we have a new ip, remember it, with timestamp
|
|
|
|
session[kind + '_timestamp'] = int(time.time())
|
2013-11-27 08:16:49 +01:00
|
|
|
session[kind] = ipaddr
|
|
|
|
session[kind + '_rdns'] = rev_lookup(ipaddr) # may be slow
|
2013-11-14 02:11:44 +01:00
|
|
|
else:
|
|
|
|
old_timestamp = session.get(kind + '_timestamp')
|
|
|
|
if not max_age or old_timestamp is None or old_timestamp + max_age < int(time.time()):
|
|
|
|
# keep it fresh (to avoid that it gets killed and triggers detection)
|
|
|
|
session[kind + '_timestamp'] = int(time.time())
|
|
|
|
if save and session.modified:
|
|
|
|
session.save()
|