1 # helper for DNS management tool
3 # Copyright (C) Amitay Isaacs 2011-2012
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 from samba
.dcerpc
import dnsserver
, dnsp
22 from samba
import WERRORError
, werror
24 # Note: these are not quite the same as similar looking classes in
25 # provision/sambadns.py -- those ones are based on
26 # dnsp.DnssrvRpcRecord, these are based on dnsserver.DNS_RPC_RECORD.
27 # They encode the same information in slightly different ways.
29 # DNS_RPC_RECORD structures ([MS-DNSP]2.2.2.2.5 "DNS_RPC_RECORD") are
30 # used on the wire by DnssrvEnumRecords2. The dnsp.DnssrvRpcRecord
31 # versions have the in-database version of the same information, where
32 # the flags field is unpacked, and the struct ordering is different.
33 # See [MS-DNSP] 2.3.2.2 "DnsRecord".
35 # In both cases the structure and contents of .data depend on .wType.
36 # For example, if .wType is DNS_TYPE_A, .data is an IPv4 address. If
37 # the .wType is changed to DNS_TYPE_CNAME, the contents of .data will
38 # be interpreted as a cname blob, but the bytes there will still be
39 # those of the IPv4 address. If you don't also set the .data you may
40 # encounter stability problems. These DNS_RPC_RECORD subclasses
41 # attempt to hide that from you, but are only pretending -- any of
42 # them can represent any type of record.
45 class DNSParseError(ValueError):
49 class ARecord(dnsserver
.DNS_RPC_RECORD
):
50 def __init__(self
, ip_addr
, serial
=1, ttl
=900, rank
=dnsp
.DNS_RANK_ZONE
,
52 super(ARecord
, self
).__init
__()
53 self
.wType
= dnsp
.DNS_TYPE_A
54 self
.dwFlags
= rank | node_flag
55 self
.dwSerial
= serial
56 self
.dwTtlSeconds
= ttl
60 def from_string(cls
, data
, sep
=None, **kwargs
):
61 return cls(data
, **kwargs
)
64 class AAAARecord(dnsserver
.DNS_RPC_RECORD
):
66 def __init__(self
, ip6_addr
, serial
=1, ttl
=900, rank
=dnsp
.DNS_RANK_ZONE
,
68 super(AAAARecord
, self
).__init
__()
69 self
.wType
= dnsp
.DNS_TYPE_AAAA
70 self
.dwFlags
= rank | node_flag
71 self
.dwSerial
= serial
72 self
.dwTtlSeconds
= ttl
76 def from_string(cls
, data
, sep
=None, **kwargs
):
77 return cls(data
, **kwargs
)
80 class PTRRecord(dnsserver
.DNS_RPC_RECORD
):
82 def __init__(self
, ptr
, serial
=1, ttl
=900, rank
=dnsp
.DNS_RANK_ZONE
,
84 super(PTRRecord
, self
).__init
__()
85 self
.wType
= dnsp
.DNS_TYPE_PTR
86 self
.dwFlags
= rank | node_flag
87 self
.dwSerial
= serial
88 self
.dwTtlSeconds
= ttl
89 ptr_name
= dnsserver
.DNS_RPC_NAME()
91 ptr_name
.len = len(ptr
)
95 def from_string(cls
, data
, sep
=None, **kwargs
):
96 return cls(data
, **kwargs
)
99 class CNAMERecord(dnsserver
.DNS_RPC_RECORD
):
101 def __init__(self
, cname
, serial
=1, ttl
=900, rank
=dnsp
.DNS_RANK_ZONE
,
104 self
.wType
= dnsp
.DNS_TYPE_CNAME
105 self
.dwFlags
= rank | node_flag
106 self
.dwSerial
= serial
107 self
.dwTtlSeconds
= ttl
108 cname_name
= dnsserver
.DNS_RPC_NAME()
109 cname_name
.str = cname
110 cname_name
.len = len(cname
)
111 self
.data
= cname_name
114 def from_string(cls
, data
, sep
=None, **kwargs
):
115 return cls(data
, **kwargs
)
118 class NSRecord(dnsserver
.DNS_RPC_RECORD
):
120 def __init__(self
, dns_server
, serial
=1, ttl
=900, rank
=dnsp
.DNS_RANK_ZONE
,
122 super(NSRecord
, self
).__init
__()
123 self
.wType
= dnsp
.DNS_TYPE_NS
124 self
.dwFlags
= rank | node_flag
125 self
.dwSerial
= serial
126 self
.dwTtlSeconds
= ttl
127 ns
= dnsserver
.DNS_RPC_NAME()
129 ns
.len = len(dns_server
)
133 def from_string(cls
, data
, sep
=None, **kwargs
):
134 return cls(data
, **kwargs
)
137 class MXRecord(dnsserver
.DNS_RPC_RECORD
):
139 def __init__(self
, mail_server
, preference
, serial
=1, ttl
=900,
140 rank
=dnsp
.DNS_RANK_ZONE
, node_flag
=0):
141 super(MXRecord
, self
).__init
__()
142 self
.wType
= dnsp
.DNS_TYPE_MX
143 self
.dwFlags
= rank | node_flag
144 self
.dwSerial
= serial
145 self
.dwTtlSeconds
= ttl
146 mx
= dnsserver
.DNS_RPC_RECORD_NAME_PREFERENCE()
147 mx
.wPreference
= preference
148 mx
.nameExchange
.str = mail_server
149 mx
.nameExchange
.len = len(mail_server
)
153 def from_string(cls
, data
, sep
=None, **kwargs
):
155 server
, priority
= data
.split(sep
)
156 priority
= int(priority
)
157 except ValueError as e
:
158 raise DNSParseError("MX data must have server and priority "
159 "(space separated), not %r" % data
) from e
160 return cls(server
, priority
, **kwargs
)
163 class SOARecord(dnsserver
.DNS_RPC_RECORD
):
165 def __init__(self
, mname
, rname
, serial
=1, refresh
=900, retry
=600,
166 expire
=86400, minimum
=3600, ttl
=3600, rank
=dnsp
.DNS_RANK_ZONE
,
167 node_flag
=dnsp
.DNS_RPC_FLAG_AUTH_ZONE_ROOT
):
168 super(SOARecord
, self
).__init
__()
169 self
.wType
= dnsp
.DNS_TYPE_SOA
170 self
.dwFlags
= rank | node_flag
171 self
.dwSerial
= serial
172 self
.dwTtlSeconds
= ttl
173 soa
= dnsserver
.DNS_RPC_RECORD_SOA()
174 soa
.dwSerialNo
= serial
175 soa
.dwRefresh
= refresh
177 soa
.dwExpire
= expire
178 soa
.dwMinimumTtl
= minimum
179 soa
.NamePrimaryServer
.str = mname
180 soa
.NamePrimaryServer
.len = len(mname
)
181 soa
.ZoneAdministratorEmail
.str = rname
182 soa
.ZoneAdministratorEmail
.len = len(rname
)
186 def from_string(cls
, data
, sep
=None, **kwargs
):
187 args
= data
.split(sep
)
189 raise DNSParseError('Data requires 7 space separated elements - '
190 'nameserver, email, serial, '
191 'refresh, retry, expire, minimumttl')
193 for i
in range(2, 7):
194 args
[i
] = int(args
[i
])
195 except ValueError as e
:
196 raise DNSParseError("SOA serial, refresh, retry, expire, minimumttl' "
197 "should be integers") from e
198 return cls(*args
, **kwargs
)
201 class SRVRecord(dnsserver
.DNS_RPC_RECORD
):
203 def __init__(self
, target
, port
, priority
=0, weight
=100, serial
=1, ttl
=900,
204 rank
=dnsp
.DNS_RANK_ZONE
, node_flag
=0):
205 super(SRVRecord
, self
).__init
__()
206 self
.wType
= dnsp
.DNS_TYPE_SRV
207 self
.dwFlags
= rank | node_flag
208 self
.dwSerial
= serial
209 self
.dwTtlSeconds
= ttl
210 srv
= dnsserver
.DNS_RPC_RECORD_SRV()
211 srv
.wPriority
= priority
214 srv
.nameTarget
.str = target
215 srv
.nameTarget
.len = len(target
)
219 def from_string(cls
, data
, sep
=None, **kwargs
):
221 target
, port
, priority
, weight
= data
.split(sep
)
222 except ValueError as e
:
223 raise DNSParseError("SRV data must have four space "
224 "separated elements: "
225 "server, port, priority, weight; "
226 "not %r" % data
) from e
228 args
= (target
, int(port
), int(priority
), int(weight
))
229 except ValueError as e
:
230 raise DNSParseError("SRV port, priority, and weight "
231 "must be integers") from e
233 return cls(*args
, **kwargs
)
236 class TXTRecord(dnsserver
.DNS_RPC_RECORD
):
238 def __init__(self
, slist
, serial
=1, ttl
=900, rank
=dnsp
.DNS_RANK_ZONE
,
240 super(TXTRecord
, self
).__init
__()
241 self
.wType
= dnsp
.DNS_TYPE_TXT
242 self
.dwFlags
= rank | node_flag
243 self
.dwSerial
= serial
244 self
.dwTtlSeconds
= ttl
245 if isinstance(slist
, str):
249 name
= dnsserver
.DNS_RPC_NAME()
253 txt
= dnsserver
.DNS_RPC_RECORD_STRING()
254 txt
.count
= len(slist
)
259 def from_string(cls
, data
, sep
=None, **kwargs
):
260 slist
= shlex
.split(data
)
261 return cls(slist
, **kwargs
)
265 # Don't add new Record types after this line
267 _RECORD_TYPE_LUT
= {}
268 def _setup_record_type_lut():
269 for k
, v
in globals().items():
270 if k
[-6:] == 'Record':
272 flag
= getattr(dnsp
, 'DNS_TYPE_' + k
)
273 _RECORD_TYPE_LUT
[k
] = v
274 _RECORD_TYPE_LUT
[flag
] = v
276 _setup_record_type_lut()
277 del _setup_record_type_lut
280 def record_from_string(t
, data
, sep
=None, **kwargs
):
281 """Get a DNS record of type t based on the data string.
282 Additional keywords (ttl, rank, etc) can be passed in.
284 t can be a dnsp.DNS_TYPE_* integer or a string like "A", "TXT", etc.
286 if isinstance(t
, str):
289 Record
= _RECORD_TYPE_LUT
[t
]
290 except KeyError as e
:
291 raise DNSParseError("Unsupported record type") from e
293 return Record
.from_string(data
, sep
=sep
, **kwargs
)
296 def flag_from_string(rec_type
):
297 rtype
= rec_type
.upper()
299 return getattr(dnsp
, 'DNS_TYPE_' + rtype
)
300 except AttributeError as e
:
301 raise DNSParseError('Unknown type of DNS record %s' % rec_type
) from e
304 def recbuf_from_string(*args
, **kwargs
):
305 rec
= record_from_string(*args
, **kwargs
)
306 buf
= dnsserver
.DNS_RPC_RECORD_BUF()
311 def dns_name_equal(n1
, n2
):
312 """Match dns name (of type DNS_RPC_NAME)"""
313 return n1
.str.rstrip('.').lower() == n2
.str.rstrip('.').lower()
316 def ipv6_normalise(addr
):
317 """Convert an AAAA address into a canonical form."""
318 packed
= socket
.inet_pton(socket
.AF_INET6
, addr
)
319 return socket
.inet_ntop(socket
.AF_INET6
, packed
)
322 def dns_record_match(dns_conn
, server
, zone
, name
, record_type
, data
):
323 """Find a dns record that matches the specified data"""
325 # The matching is not as precises as that offered by
326 # dsdb_dns.match_record, which, for example, compares IPv6 records
327 # semantically rather than as strings. However that function
328 # compares database DnssrvRpcRecord structures, not wire
329 # DNS_RPC_RECORD structures.
331 # While it would be possible, perhaps desirable, to wrap that
332 # function for use in samba-tool, there is value in having a
333 # separate implementation for tests, to avoid the circularity of
334 # asserting the function matches itself.
336 urec
= record_from_string(record_type
, data
)
338 select_flags
= dnsserver
.DNS_RPC_VIEW_AUTHORITY_DATA
341 buflen
, res
= dns_conn
.DnssrvEnumRecords2(
342 dnsserver
.DNS_CLIENT_VERSION_LONGHORN
, 0, server
, zone
, name
, None,
343 record_type
, select_flags
, None, None)
344 except WERRORError
as e
:
345 if e
.args
[0] == werror
.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
:
346 # Either the zone doesn't exist, or there were no records.
347 # We can't differentiate the two.
351 if not res
or res
.count
== 0:
354 for rec
in res
.rec
[0].records
:
355 if rec
.wType
!= record_type
:
359 if record_type
== dnsp
.DNS_TYPE_A
:
360 if rec
.data
== urec
.data
:
362 elif record_type
== dnsp
.DNS_TYPE_AAAA
:
363 if ipv6_normalise(rec
.data
) == ipv6_normalise(urec
.data
):
365 elif record_type
== dnsp
.DNS_TYPE_PTR
:
366 if dns_name_equal(rec
.data
, urec
.data
):
368 elif record_type
== dnsp
.DNS_TYPE_CNAME
:
369 if dns_name_equal(rec
.data
, urec
.data
):
371 elif record_type
== dnsp
.DNS_TYPE_NS
:
372 if dns_name_equal(rec
.data
, urec
.data
):
374 elif record_type
== dnsp
.DNS_TYPE_MX
:
375 if dns_name_equal(rec
.data
.nameExchange
, urec
.data
.nameExchange
) and \
376 rec
.data
.wPreference
== urec
.data
.wPreference
:
378 elif record_type
== dnsp
.DNS_TYPE_SRV
:
379 if rec
.data
.wPriority
== urec
.data
.wPriority
and \
380 rec
.data
.wWeight
== urec
.data
.wWeight
and \
381 rec
.data
.wPort
== urec
.data
.wPort
and \
382 dns_name_equal(rec
.data
.nameTarget
, urec
.data
.nameTarget
):
384 elif record_type
== dnsp
.DNS_TYPE_SOA
:
385 if rec
.data
.dwSerialNo
== urec
.data
.dwSerialNo
and \
386 rec
.data
.dwRefresh
== urec
.data
.dwRefresh
and \
387 rec
.data
.dwRetry
== urec
.data
.dwRetry
and \
388 rec
.data
.dwExpire
== urec
.data
.dwExpire
and \
389 rec
.data
.dwMinimumTtl
== urec
.data
.dwMinimumTtl
and \
390 dns_name_equal(rec
.data
.NamePrimaryServer
,
391 urec
.data
.NamePrimaryServer
) and \
392 dns_name_equal(rec
.data
.ZoneAdministratorEmail
,
393 urec
.data
.ZoneAdministratorEmail
):
395 elif record_type
== dnsp
.DNS_TYPE_TXT
:
396 if rec
.data
.count
== urec
.data
.count
:
398 for i
in range(rec
.data
.count
):
400 (rec
.data
.str[i
].str == urec
.data
.str[i
].str)