2017-02-17 08:21:33 +03:00
# helper for DNS management tool
#
# Copyright (C) Amitay Isaacs 2011-2012
#
# 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/>.
#
2021-04-07 04:34:50 +03:00
import shlex
2021-06-18 02:30:09 +03:00
import socket
2017-02-17 08:21:33 +03:00
from samba . dcerpc import dnsserver , dnsp
2021-11-19 06:11:14 +03:00
from samba import WERRORError , werror
2017-02-17 08:21:33 +03:00
2021-03-26 10:42:49 +03:00
# Note: these are not quite the same as similar looking classes in
# provision/sambadns.py -- those ones are based on
# dnsp.DnssrvRpcRecord, these are based on dnsserver.DNS_RPC_RECORD.
# They encode the same information in slightly different ways.
2017-02-17 08:21:33 +03:00
#
2021-03-26 10:42:49 +03:00
# DNS_RPC_RECORD structures ([MS-DNSP]2.2.2.2.5 "DNS_RPC_RECORD") are
# used on the wire by DnssrvEnumRecords2. The dnsp.DnssrvRpcRecord
# versions have the in-database version of the same information, where
# the flags field is unpacked, and the struct ordering is different.
# See [MS-DNSP] 2.3.2.2 "DnsRecord".
2017-02-17 08:21:33 +03:00
#
2021-03-26 10:42:49 +03:00
# In both cases the structure and contents of .data depend on .wType.
# For example, if .wType is DNS_TYPE_A, .data is an IPv4 address. If
# the .wType is changed to DNS_TYPE_CNAME, the contents of .data will
# be interpreted as a cname blob, but the bytes there will still be
# those of the IPv4 address. If you don't also set the .data you may
# encounter stability problems. These DNS_RPC_RECORD subclasses
# attempt to hide that from you, but are only pretending -- any of
# them can represent any type of record.
2017-02-17 08:21:33 +03:00
2021-04-07 04:34:50 +03:00
class DNSParseError ( ValueError ) :
pass
2018-07-30 09:20:39 +03:00
2017-02-17 08:21:33 +03:00
class ARecord ( dnsserver . DNS_RPC_RECORD ) :
def __init__ ( self , ip_addr , serial = 1 , ttl = 900 , rank = dnsp . DNS_RANK_ZONE ,
2018-07-30 09:15:34 +03:00
node_flag = 0 ) :
2017-02-17 08:21:33 +03:00
super ( ARecord , self ) . __init__ ( )
self . wType = dnsp . DNS_TYPE_A
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
2021-04-01 05:26:01 +03:00
self . data = ip_addr
2017-02-17 08:21:33 +03:00
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
return cls ( data , * * kwargs )
2017-02-17 08:21:33 +03:00
class AAAARecord ( dnsserver . DNS_RPC_RECORD ) :
def __init__ ( self , ip6_addr , serial = 1 , ttl = 900 , rank = dnsp . DNS_RANK_ZONE ,
2018-07-30 09:15:34 +03:00
node_flag = 0 ) :
2017-02-17 08:21:33 +03:00
super ( AAAARecord , self ) . __init__ ( )
self . wType = dnsp . DNS_TYPE_AAAA
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
2021-04-01 05:26:01 +03:00
self . data = ip6_addr
2017-02-17 08:21:33 +03:00
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
return cls ( data , * * kwargs )
2017-02-17 08:21:33 +03:00
class PTRRecord ( dnsserver . DNS_RPC_RECORD ) :
def __init__ ( self , ptr , serial = 1 , ttl = 900 , rank = dnsp . DNS_RANK_ZONE ,
node_flag = 0 ) :
super ( PTRRecord , self ) . __init__ ( )
self . wType = dnsp . DNS_TYPE_PTR
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
ptr_name = dnsserver . DNS_RPC_NAME ( )
2021-04-01 05:26:01 +03:00
ptr_name . str = ptr
2017-02-17 08:21:33 +03:00
ptr_name . len = len ( ptr )
self . data = ptr_name
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
return cls ( data , * * kwargs )
2017-02-17 08:21:33 +03:00
2021-03-26 08:22:17 +03:00
class CNAMERecord ( dnsserver . DNS_RPC_RECORD ) :
2017-02-17 08:21:33 +03:00
def __init__ ( self , cname , serial = 1 , ttl = 900 , rank = dnsp . DNS_RANK_ZONE ,
2018-07-30 09:15:34 +03:00
node_flag = 0 ) :
2021-03-26 08:22:17 +03:00
super ( ) . __init__ ( )
2017-02-17 08:21:33 +03:00
self . wType = dnsp . DNS_TYPE_CNAME
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
cname_name = dnsserver . DNS_RPC_NAME ( )
2021-04-01 05:26:01 +03:00
cname_name . str = cname
2017-02-17 08:21:33 +03:00
cname_name . len = len ( cname )
self . data = cname_name
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
return cls ( data , * * kwargs )
2017-02-17 08:21:33 +03:00
class NSRecord ( dnsserver . DNS_RPC_RECORD ) :
def __init__ ( self , dns_server , serial = 1 , ttl = 900 , rank = dnsp . DNS_RANK_ZONE ,
2018-07-30 09:15:34 +03:00
node_flag = 0 ) :
2017-02-17 08:21:33 +03:00
super ( NSRecord , self ) . __init__ ( )
self . wType = dnsp . DNS_TYPE_NS
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
ns = dnsserver . DNS_RPC_NAME ( )
2021-04-01 05:26:01 +03:00
ns . str = dns_server
2017-02-17 08:21:33 +03:00
ns . len = len ( dns_server )
self . data = ns
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
return cls ( data , * * kwargs )
2017-02-17 08:21:33 +03:00
class MXRecord ( dnsserver . DNS_RPC_RECORD ) :
def __init__ ( self , mail_server , preference , serial = 1 , ttl = 900 ,
rank = dnsp . DNS_RANK_ZONE , node_flag = 0 ) :
super ( MXRecord , self ) . __init__ ( )
self . wType = dnsp . DNS_TYPE_MX
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
mx = dnsserver . DNS_RPC_RECORD_NAME_PREFERENCE ( )
mx . wPreference = preference
2021-04-01 05:26:01 +03:00
mx . nameExchange . str = mail_server
2017-02-17 08:21:33 +03:00
mx . nameExchange . len = len ( mail_server )
self . data = mx
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
try :
server , priority = data . split ( sep )
priority = int ( priority )
except ValueError as e :
raise DNSParseError ( " MX data must have server and priority "
" (space separated), not %r " % data ) from e
return cls ( server , priority , * * kwargs )
2017-02-17 08:21:33 +03:00
class SOARecord ( dnsserver . DNS_RPC_RECORD ) :
def __init__ ( self , mname , rname , serial = 1 , refresh = 900 , retry = 600 ,
expire = 86400 , minimum = 3600 , ttl = 3600 , rank = dnsp . DNS_RANK_ZONE ,
node_flag = dnsp . DNS_RPC_FLAG_AUTH_ZONE_ROOT ) :
super ( SOARecord , self ) . __init__ ( )
self . wType = dnsp . DNS_TYPE_SOA
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
soa = dnsserver . DNS_RPC_RECORD_SOA ( )
soa . dwSerialNo = serial
soa . dwRefresh = refresh
soa . dwRetry = retry
soa . dwExpire = expire
soa . dwMinimumTtl = minimum
2021-04-01 05:26:01 +03:00
soa . NamePrimaryServer . str = mname
2017-02-17 08:21:33 +03:00
soa . NamePrimaryServer . len = len ( mname )
2021-04-01 05:26:01 +03:00
soa . ZoneAdministratorEmail . str = rname
2017-02-17 08:21:33 +03:00
soa . ZoneAdministratorEmail . len = len ( rname )
self . data = soa
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
args = data . split ( sep )
if len ( args ) != 7 :
raise DNSParseError ( ' Data requires 7 space separated elements - '
' nameserver, email, serial, '
' refresh, retry, expire, minimumttl ' )
try :
for i in range ( 2 , 7 ) :
args [ i ] = int ( args [ i ] )
except ValueError as e :
raise DNSParseError ( " SOA serial, refresh, retry, expire, minimumttl ' "
" should be integers " ) from e
return cls ( * args , * * kwargs )
2017-02-17 08:21:33 +03:00
class SRVRecord ( dnsserver . DNS_RPC_RECORD ) :
def __init__ ( self , target , port , priority = 0 , weight = 100 , serial = 1 , ttl = 900 ,
2018-07-30 09:16:12 +03:00
rank = dnsp . DNS_RANK_ZONE , node_flag = 0 ) :
2017-02-17 08:21:33 +03:00
super ( SRVRecord , self ) . __init__ ( )
self . wType = dnsp . DNS_TYPE_SRV
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
srv = dnsserver . DNS_RPC_RECORD_SRV ( )
srv . wPriority = priority
srv . wWeight = weight
srv . wPort = port
2021-04-01 05:26:01 +03:00
srv . nameTarget . str = target
2017-02-17 08:21:33 +03:00
srv . nameTarget . len = len ( target )
self . data = srv
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
try :
target , port , priority , weight = data . split ( sep )
except ValueError as e :
raise DNSParseError ( " SRV data must have four space "
" separated elements: "
" server, port, priority, weight; "
" not %r " % data ) from e
try :
args = ( target , int ( port ) , int ( priority ) , int ( weight ) )
except ValueError as e :
raise DNSParseError ( " SRV port, priority, and weight "
" must be integers " ) from e
return cls ( * args , * * kwargs )
2017-02-17 08:21:33 +03:00
class TXTRecord ( dnsserver . DNS_RPC_RECORD ) :
def __init__ ( self , slist , serial = 1 , ttl = 900 , rank = dnsp . DNS_RANK_ZONE ,
2018-07-30 09:16:12 +03:00
node_flag = 0 ) :
2017-02-17 08:21:33 +03:00
super ( TXTRecord , self ) . __init__ ( )
self . wType = dnsp . DNS_TYPE_TXT
self . dwFlags = rank | node_flag
self . dwSerial = serial
self . dwTtlSeconds = ttl
2021-05-19 06:43:14 +03:00
if isinstance ( slist , str ) :
slist = [ slist ]
2017-02-17 08:21:33 +03:00
names = [ ]
2021-04-01 05:26:01 +03:00
for s in slist :
2017-02-17 08:21:33 +03:00
name = dnsserver . DNS_RPC_NAME ( )
name . str = s
name . len = len ( s )
names . append ( name )
txt = dnsserver . DNS_RPC_RECORD_STRING ( )
txt . count = len ( slist )
txt . str = names
self . data = txt
2021-04-07 04:34:50 +03:00
@classmethod
def from_string ( cls , data , sep = None , * * kwargs ) :
slist = shlex . split ( data )
return cls ( slist , * * kwargs )
2021-04-01 05:28:49 +03:00
#
# Don't add new Record types after this line
_RECORD_TYPE_LUT = { }
def _setup_record_type_lut ( ) :
for k , v in globals ( ) . items ( ) :
if k [ - 6 : ] == ' Record ' :
k = k [ : - 6 ]
flag = getattr ( dnsp , ' DNS_TYPE_ ' + k )
_RECORD_TYPE_LUT [ k ] = v
_RECORD_TYPE_LUT [ flag ] = v
_setup_record_type_lut ( )
del _setup_record_type_lut
def record_from_string ( t , data , sep = None , * * kwargs ) :
""" Get a DNS record of type t based on the data string.
Additional keywords ( ttl , rank , etc ) can be passed in .
t can be a dnsp . DNS_TYPE_ * integer or a string like " A " , " TXT " , etc .
"""
if isinstance ( t , str ) :
t = t . upper ( )
try :
Record = _RECORD_TYPE_LUT [ t ]
except KeyError as e :
raise DNSParseError ( " Unsupported record type " ) from e
return Record . from_string ( data , sep = sep , * * kwargs )
2021-04-01 05:29:32 +03:00
def flag_from_string ( rec_type ) :
rtype = rec_type . upper ( )
try :
return getattr ( dnsp , ' DNS_TYPE_ ' + rtype )
2021-11-19 06:21:08 +03:00
except AttributeError as e :
2021-04-01 05:29:32 +03:00
raise DNSParseError ( ' Unknown type of DNS record %s ' % rec_type ) from e
2021-05-27 00:45:18 +03:00
2021-05-13 06:51:45 +03:00
def recbuf_from_string ( * args , * * kwargs ) :
rec = record_from_string ( * args , * * kwargs )
buf = dnsserver . DNS_RPC_RECORD_BUF ( )
buf . rec = rec
return buf
2021-05-27 00:45:18 +03:00
def dns_name_equal ( n1 , n2 ) :
""" Match dns name (of type DNS_RPC_NAME) """
return n1 . str . rstrip ( ' . ' ) . lower ( ) == n2 . str . rstrip ( ' . ' ) . lower ( )
2021-06-18 02:30:09 +03:00
def ipv6_normalise ( addr ) :
""" Convert an AAAA adresss into a canonical form. """
packed = socket . inet_pton ( socket . AF_INET6 , addr )
return socket . inet_ntop ( socket . AF_INET6 , packed )
2021-05-27 00:45:18 +03:00
def dns_record_match ( dns_conn , server , zone , name , record_type , data ) :
""" Find a dns record that matches the specified data """
# The matching is not as precises as that offered by
# dsdb_dns.match_record, which, for example, compares IPv6 records
# semantically rather than as strings. However that function
# compares database DnssrvRpcRecord structures, not wire
# DNS_RPC_RECORD structures.
#
# While it would be possible, perhaps desirable, to wrap that
# function for use in samba-tool, there is value in having a
# separate implementation for tests, to avoid the circularity of
# asserting the function matches itself.
urec = record_from_string ( record_type , data )
select_flags = dnsserver . DNS_RPC_VIEW_AUTHORITY_DATA
try :
buflen , res = dns_conn . DnssrvEnumRecords2 (
dnsserver . DNS_CLIENT_VERSION_LONGHORN , 0 , server , zone , name , None ,
record_type , select_flags , None , None )
except WERRORError as e :
if e . args [ 0 ] == werror . WERR_DNS_ERROR_NAME_DOES_NOT_EXIST :
# Either the zone doesn't exist, or there were no records.
# We can't differentiate the two.
return None
raise e
if not res or res . count == 0 :
return None
for rec in res . rec [ 0 ] . records :
if rec . wType != record_type :
continue
found = False
if record_type == dnsp . DNS_TYPE_A :
if rec . data == urec . data :
found = True
elif record_type == dnsp . DNS_TYPE_AAAA :
2021-06-18 02:30:09 +03:00
if ipv6_normalise ( rec . data ) == ipv6_normalise ( urec . data ) :
2021-05-27 00:45:18 +03:00
found = True
elif record_type == dnsp . DNS_TYPE_PTR :
if dns_name_equal ( rec . data , urec . data ) :
found = True
elif record_type == dnsp . DNS_TYPE_CNAME :
if dns_name_equal ( rec . data , urec . data ) :
found = True
elif record_type == dnsp . DNS_TYPE_NS :
if dns_name_equal ( rec . data , urec . data ) :
found = True
elif record_type == dnsp . DNS_TYPE_MX :
if dns_name_equal ( rec . data . nameExchange , urec . data . nameExchange ) and \
rec . data . wPreference == urec . data . wPreference :
found = True
elif record_type == dnsp . DNS_TYPE_SRV :
if rec . data . wPriority == urec . data . wPriority and \
rec . data . wWeight == urec . data . wWeight and \
rec . data . wPort == urec . data . wPort and \
dns_name_equal ( rec . data . nameTarget , urec . data . nameTarget ) :
found = True
elif record_type == dnsp . DNS_TYPE_SOA :
if rec . data . dwSerialNo == urec . data . dwSerialNo and \
rec . data . dwRefresh == urec . data . dwRefresh and \
rec . data . dwRetry == urec . data . dwRetry and \
rec . data . dwExpire == urec . data . dwExpire and \
rec . data . dwMinimumTtl == urec . data . dwMinimumTtl and \
dns_name_equal ( rec . data . NamePrimaryServer ,
urec . data . NamePrimaryServer ) and \
dns_name_equal ( rec . data . ZoneAdministratorEmail ,
urec . data . ZoneAdministratorEmail ) :
found = True
elif record_type == dnsp . DNS_TYPE_TXT :
if rec . data . count == urec . data . count :
found = True
for i in range ( rec . data . count ) :
found = found and \
( rec . data . str [ i ] . str == urec . data . str [ i ] . str )
if found :
return rec
return None