|
Line 0
Link Here
|
| 0 |
- |
1 |
#!/usr/bin/python2.7 |
| 1 |
-- |
2 |
# coding: utf-8 |
|
|
3 |
# |
| 4 |
# Univention Management Console module: |
| 5 |
# System Diagnosis UMC module |
| 6 |
# |
| 7 |
# Copyright 2016-2017 Univention GmbH |
| 8 |
# |
| 9 |
# http://www.univention.de/ |
| 10 |
# |
| 11 |
# All rights reserved. |
| 12 |
# |
| 13 |
# The source code of this program is made available |
| 14 |
# under the terms of the GNU Affero General Public License version 3 |
| 15 |
# (GNU AGPL V3) as published by the Free Software Foundation. |
| 16 |
# |
| 17 |
# Binary versions of this program provided by Univention to you as |
| 18 |
# well as other copyrighted, protected or trademarked materials like |
| 19 |
# Logos, graphics, fonts, specific documentations and configurations, |
| 20 |
# cryptographic keys etc. are subject to a license agreement between |
| 21 |
# you and Univention and not subject to the GNU AGPL V3. |
| 22 |
# |
| 23 |
# In the case you use this program under the terms of the GNU AGPL V3, |
| 24 |
# the program is provided in the hope that it will be useful, |
| 25 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 26 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 27 |
# GNU Affero General Public License for more details. |
| 28 |
# |
| 29 |
# You should have received a copy of the GNU Affero General Public |
| 30 |
# License with the Debian GNU/Linux or Univention distribution in file |
| 31 |
# /usr/share/common-licenses/AGPL-3; if not, see |
| 32 |
# <http://www.gnu.org/licenses/>. |
| 33 |
|
| 34 |
import struct |
| 35 |
import socket |
| 36 |
import random |
| 37 |
|
| 38 |
import ldap |
| 39 |
import ipaddr |
| 40 |
import dns.resolver |
| 41 |
from pyasn1.type import tag |
| 42 |
from pyasn1.type import char |
| 43 |
from pyasn1.type import univ |
| 44 |
from pyasn1.type import useful |
| 45 |
from pyasn1.type import namedtype |
| 46 |
import pyasn1.codec.der.encoder |
| 47 |
import pyasn1.codec.der.decoder |
| 48 |
import pyasn1.error |
| 49 |
|
| 50 |
from univention.config_registry import handler_set as ucr_set |
| 51 |
import univention.config_registry |
| 52 |
from univention.management.console.modules.diagnostic import Warning, Critical, ProblemFixed |
| 53 |
|
| 54 |
from univention.lib.i18n import Translation |
| 55 |
_ = Translation('univention-management-console-module-diagnostic').translate |
| 56 |
|
| 57 |
title = _('KDC service check') |
| 58 |
description = _('The check for the KDC reachability was succesful.') |
| 59 |
|
| 60 |
|
| 61 |
# This checks for the reachability of KDCs by sending a AS-REQ per TCP and UDP. |
| 62 |
# The AS-REQ is send with the fake user `kdc-reachability-check`. The KDCs will |
| 63 |
# respond in several ways: either with an KRB-ERROR (PREAUTH_REQUIRED, |
| 64 |
# PRINCIPAL_UNKNOWN or RESPONSE_TO_BIG) or a AS-REP with an anonymous ticket. |
| 65 |
# |
| 66 |
# If we do not receive one of the above, the connection is not accepted, the |
| 67 |
# socket is closed or an operation times out, we can assume, that the KDCs is |
| 68 |
# not reachable. |
| 69 |
# |
| 70 |
# This check will test the KDCs as specified in UCR `kerberos/kdc` with TCP and |
| 71 |
# UDP on port 88. If `kerberos/defaults/dns_lookup_kdc` is set, KDC discovery as |
| 72 |
# specified in section `7.2.3. KDC Discovery on IP Networks` [1] will be used. |
| 73 |
# In this case the ports as specified in the SRV records are used. |
| 74 |
# |
| 75 |
# This implements a minimal number of packages as defined in [1] and does not |
| 76 |
# rely on python-kerberos or python-krb5, as those are too high level and |
| 77 |
# outdated. |
| 78 |
# |
| 79 |
# Reachability checks of kpasswd servers are not implemented, as those are a |
| 80 |
# separate protocol. See [2]. |
| 81 |
# |
| 82 |
# [1]: https://tools.ietf.org/html/rfc4120 |
| 83 |
# [2]: https://tools.ietf.org/html/rfc3244 |
| 84 |
|
| 85 |
|
| 86 |
def add_lo_to_samba_interfaces(): |
| 87 |
configRegistry = univention.config_registry.ConfigRegistry() |
| 88 |
configRegistry.load() |
| 89 |
|
| 90 |
interfaces = configRegistry.get('samba/interfaces', '').split() |
| 91 |
interfaces.append('lo') |
| 92 |
ucr_set(['samba/interfaces={}'.format(' '.join(interfaces))]) |
| 93 |
return run(retest=True) |
| 94 |
|
| 95 |
|
| 96 |
def reset_kerberos_kdc(): |
| 97 |
ucr_set(['kerberos/kdc=127.0.0.1']) |
| 98 |
return run(retest=True) |
| 99 |
|
| 100 |
|
| 101 |
actions = { |
| 102 |
'add_lo_to_samba_interfaces': add_lo_to_samba_interfaces, |
| 103 |
'reset_kerberos_kdc': reset_kerberos_kdc, |
| 104 |
} |
| 105 |
|
| 106 |
|
| 107 |
def _c(n, t): |
| 108 |
return t.clone(tagSet=t.tagSet + tag.Tag(tag.tagClassContext, tag.tagFormatSimple, n)) |
| 109 |
|
| 110 |
|
| 111 |
class PrincipalName(univ.Sequence): |
| 112 |
componentType = namedtype.NamedTypes( |
| 113 |
namedtype.NamedType('name-type', _c(0, univ.Integer())), |
| 114 |
namedtype.NamedType('name-string', _c(1, univ.SequenceOf(componentType=char.GeneralString())))) |
| 115 |
|
| 116 |
|
| 117 |
class KdcReqBody(univ.Sequence): |
| 118 |
tagSet = univ.Sequence.tagSet + tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4) |
| 119 |
componentType = namedtype.NamedTypes( |
| 120 |
namedtype.NamedType('kdc-options', _c(0, univ.BitString())), |
| 121 |
namedtype.OptionalNamedType('cname', _c(1, PrincipalName())), |
| 122 |
namedtype.NamedType('realm', _c(2, char.GeneralString())), |
| 123 |
namedtype.OptionalNamedType('sname', _c(3, PrincipalName())), |
| 124 |
namedtype.NamedType('till', _c(5, useful.GeneralizedTime())), |
| 125 |
namedtype.NamedType('nonce', _c(7, univ.Integer())), |
| 126 |
namedtype.NamedType('etype', _c(8, univ.SequenceOf(componentType=univ.Integer())))) |
| 127 |
|
| 128 |
|
| 129 |
class PAData(univ.Sequence): |
| 130 |
componentType = namedtype.NamedTypes( |
| 131 |
namedtype.NamedType('padata-type', _c(1, univ.Integer())), |
| 132 |
namedtype.NamedType('padata-value', _c(2, univ.OctetString()))) |
| 133 |
|
| 134 |
|
| 135 |
class AsReq(univ.Sequence): |
| 136 |
tagSet = univ.Sequence.tagSet + tag.Tag(tag.tagClassApplication, tag.tagFormatSimple, 10) |
| 137 |
componentType = namedtype.NamedTypes( |
| 138 |
namedtype.NamedType('pvno', _c(1, univ.Integer())), |
| 139 |
namedtype.NamedType('msg-type', _c(2, univ.Integer())), |
| 140 |
namedtype.NamedType('padata', _c(3, univ.SequenceOf(componentType=PAData()))), |
| 141 |
namedtype.NamedType('req-body', KdcReqBody())) |
| 142 |
|
| 143 |
|
| 144 |
class AsRep(univ.Sequence): |
| 145 |
tagSet = univ.Sequence.tagSet + tag.Tag(tag.tagClassApplication, tag.tagFormatSimple, 11) |
| 146 |
componentType = namedtype.NamedTypes( |
| 147 |
namedtype.NamedType('pvno', _c(0, univ.Integer())), |
| 148 |
namedtype.NamedType('msg-type', _c(1, univ.Integer())) |
| 149 |
# some more omitted |
| 150 |
) |
| 151 |
|
| 152 |
|
| 153 |
class KrbError(univ.Sequence): |
| 154 |
tagSet = univ.Sequence.tagSet + tag.Tag(tag.tagClassApplication, tag.tagFormatSimple, 30) |
| 155 |
componentType = namedtype.NamedTypes( |
| 156 |
namedtype.NamedType('pvno', _c(0, univ.Integer())), |
| 157 |
namedtype.NamedType('msg-type', _c(1, univ.Integer())) |
| 158 |
# some more omitted |
| 159 |
) |
| 160 |
|
| 161 |
|
| 162 |
class KerberosException(Exception): |
| 163 |
pass |
| 164 |
|
| 165 |
|
| 166 |
class ServerUnreachable(KerberosException): |
| 167 |
pass |
| 168 |
|
| 169 |
|
| 170 |
class InvalidResponse(KerberosException): |
| 171 |
pass |
| 172 |
|
| 173 |
|
| 174 |
class EmptyResponse(KerberosException): |
| 175 |
pass |
| 176 |
|
| 177 |
|
| 178 |
def build_kerberos_request(target_realm, user_name): |
| 179 |
req_body = KdcReqBody() |
| 180 |
req_body['kdc-options'] = "'01010000100000000000000000000000'B" |
| 181 |
|
| 182 |
req_body['cname'] = None |
| 183 |
req_body['cname']['name-type'] = 1 # NT_PRINCIPAL |
| 184 |
req_body['cname']['name-string'] = None |
| 185 |
req_body['cname']['name-string'][0] = user_name |
| 186 |
|
| 187 |
req_body['realm'] = target_realm |
| 188 |
|
| 189 |
req_body['sname'] = None |
| 190 |
req_body['sname']['name-type'] = 2 # NT_SRV_INST |
| 191 |
req_body['sname']['name-string'] = None |
| 192 |
req_body['sname']['name-string'][0] = 'krbtgt' |
| 193 |
req_body['sname']['name-string'][1] = target_realm |
| 194 |
|
| 195 |
req_body['till'] = '19700101000000Z' |
| 196 |
req_body['nonce'] = random.SystemRandom().getrandbits(31) |
| 197 |
req_body['etype'] = None |
| 198 |
req_body['etype'][0] = 18 # AES256_CTS_HMAC_SHA1_96 |
| 199 |
|
| 200 |
as_req = AsReq() |
| 201 |
as_req['pvno'] = 5 |
| 202 |
as_req['msg-type'] = 10 # AS-REQ |
| 203 |
as_req['padata'] = None |
| 204 |
as_req['req-body'] = req_body |
| 205 |
|
| 206 |
return pyasn1.codec.der.encoder.encode(as_req) |
| 207 |
|
| 208 |
|
| 209 |
def send_and_receive(kdc, port, protocol, as_req): |
| 210 |
socket_type = socket.SOCK_DGRAM if protocol == 'udp' else socket.SOCK_STREAM |
| 211 |
sock = socket.socket(socket.AF_INET, socket_type) |
| 212 |
sock.settimeout(1) |
| 213 |
|
| 214 |
if protocol == 'tcp': |
| 215 |
packed = struct.pack('>I', len(as_req)) + as_req |
| 216 |
else: |
| 217 |
packed = as_req |
| 218 |
|
| 219 |
try: |
| 220 |
sock.connect((kdc, port)) |
| 221 |
sock.sendall(packed) |
| 222 |
except socket.error, socket.timeout: |
| 223 |
sock.close() |
| 224 |
raise ServerUnreachable() |
| 225 |
|
| 226 |
received = '' |
| 227 |
num_received = 0 |
| 228 |
if protocol == 'udp': # fake the length field |
| 229 |
received += '\x00\x00\x00\x00' |
| 230 |
num_received += 4 |
| 231 |
while num_received < 128: |
| 232 |
try: |
| 233 |
(buf, addr) = sock.recvfrom(128) |
| 234 |
except socket.error, socket.timeout: |
| 235 |
buf = '' |
| 236 |
if not buf: |
| 237 |
break |
| 238 |
received += buf |
| 239 |
num_received += len(buf) |
| 240 |
|
| 241 |
if not received: |
| 242 |
raise EmptyResponse() |
| 243 |
|
| 244 |
return received |
| 245 |
|
| 246 |
|
| 247 |
def probe_kdc(kdc, port, protocol, target_realm, user_name): |
| 248 |
request = build_kerberos_request(target_realm, user_name) |
| 249 |
|
| 250 |
try: |
| 251 |
received = send_and_receive(kdc, port, protocol, request) |
| 252 |
except KerberosException: |
| 253 |
return False |
| 254 |
|
| 255 |
try: |
| 256 |
(error, _sub) = pyasn1.codec.der.decoder.decode(received, asn1Spec=KrbError()) |
| 257 |
except pyasn1.error.PyAsn1Error: |
| 258 |
pass |
| 259 |
else: |
| 260 |
return True |
| 261 |
|
| 262 |
try: |
| 263 |
(rep, _sub) = pyasn1.codec.der.decoder.decode(received, asn1Spec=AsRep()) |
| 264 |
except pyasn1.error.PyAsn1Error: |
| 265 |
return False |
| 266 |
|
| 267 |
return True |
| 268 |
|
| 269 |
|
| 270 |
def is_service_active(service): |
| 271 |
lo = univention.uldap.getMachineConnection() |
| 272 |
raw_filter = '(&(univentionService=%s)(cn=%s))' |
| 273 |
filter_expr = ldap.filter.filter_format(raw_filter, (service, socket.gethostname())) |
| 274 |
for (dn, _attr) in lo.search(filter_expr, attr=['cn']): |
| 275 |
if dn is not None: |
| 276 |
return True |
| 277 |
return False |
| 278 |
|
| 279 |
|
| 280 |
def run(retest=False): |
| 281 |
configRegistry = univention.config_registry.ConfigRegistry() |
| 282 |
configRegistry.load() |
| 283 |
|
| 284 |
target_realm = configRegistry.get('kerberos/realm') |
| 285 |
user_name = 'kdc-reachability-check' |
| 286 |
|
| 287 |
kdc_fqds = configRegistry.get('kerberos/kdc', '').split() |
| 288 |
dns_lookup_kdc = configRegistry.is_true('kerberos/defaults/dns_lookup_kdc', True) |
| 289 |
if not dns_lookup_kdc and not kdc_fqds: |
| 290 |
kerberos_dns_fqdn_tcp = '_kerberos._tcp.{}'.format(configRegistry.get('domainname')) |
| 291 |
result_tcp = dns.resolver.query(kerberos_dns_fqdn_tcp, 'SRV') |
| 292 |
kdc_to_check = [(r.target.to_text(True), r.port, 'tcp') for r in result_tcp] |
| 293 |
|
| 294 |
kerberos_dns_fqdn_udp = '_kerberos._udp.{}'.format(configRegistry.get('domainname')) |
| 295 |
result_udp = dns.resolver.query(kerberos_dns_fqdn_udp, 'SRV') |
| 296 |
kdc_to_check.extend((r.target.to_text(True), r.port, 'udp') for r in result_udp) |
| 297 |
else: |
| 298 |
kdc_to_check = [(kdc, 88, 'tcp') for kdc in kdc_fqds] |
| 299 |
kdc_to_check.extend((kdc, 88, 'udp') for kdc in kdc_fqds) |
| 300 |
|
| 301 |
kdc_reachabe = [(probe_kdc(kdc, port, protocol, target_realm, user_name), |
| 302 |
(kdc, port, protocol)) for (kdc, port, protocol) in kdc_to_check] |
| 303 |
reachable_kdc = [(kdc, port, protocol) for (reachable, (kdc, port, protocol)) |
| 304 |
in kdc_reachabe if reachable] |
| 305 |
unreachable_kdc = [(kdc, port, protocol) for (reachable, (kdc, port, protocol)) |
| 306 |
in kdc_reachabe if not reachable] |
| 307 |
|
| 308 |
error_descriptions = list() |
| 309 |
|
| 310 |
if unreachable_kdc: |
| 311 |
error = _('The following KDCs were unreachable: {}') |
| 312 |
unreach_string = ('{} {}:{}'.format(protocol, kdc, port) |
| 313 |
for (kdc, port, protocol) in unreachable_kdc) |
| 314 |
error_descriptions.append(error.format(', '.join(unreach_string))) |
| 315 |
|
| 316 |
if not reachable_kdc: |
| 317 |
is_dc = configRegistry.get('server/role') == 'domaincontroller_master' |
| 318 |
is_s4_dc = is_dc and is_service_active('Samba 4') |
| 319 |
if is_s4_dc and configRegistry.is_true('samba/interfaces/bindonly', False): |
| 320 |
local_included = False |
| 321 |
for interface in configRegistry.get('samba/interfaces', '').split(): |
| 322 |
try: |
| 323 |
addr = ipaddr.IPAddress(interface) |
| 324 |
except ValueError: |
| 325 |
local_included |= interface == 'lo' |
| 326 |
else: |
| 327 |
local_included |= addr.is_loopback or addr.is_unspecified |
| 328 |
error = _('samba/interfaces does not contain lo, 127.0.0.1 or 0.0.0.0.') |
| 329 |
error_descriptions.append(error) |
| 330 |
|
| 331 |
description = '\n'.join(error_descriptions) |
| 332 |
buttons = [{ |
| 333 |
'action': 'add_lo_to_samba_interfaces', |
| 334 |
'label': _('Add lo to samba/interfaces'), |
| 335 |
}, { |
| 336 |
'action': 'reset_kerberos_kdc', |
| 337 |
'label': _('Reset kerberos/kdc to 127.0.0.1'), |
| 338 |
}] |
| 339 |
raise Critical(description=description, buttons=buttons) |
| 340 |
|
| 341 |
error_descriptions.append(_('No reachable KDCs were found.')) |
| 342 |
description = '\n'.join(error_descriptions) |
| 343 |
raise Critical(description=description) |
| 344 |
|
| 345 |
if error_descriptions: |
| 346 |
error = '\n'.join(error_descriptions) |
| 347 |
raise Warning(description=error) |
| 348 |
|
| 349 |
if retest: |
| 350 |
raise ProblemFixed() |
| 351 |
|
| 352 |
|
| 353 |
if __name__ == '__main__': |
| 354 |
from univention.management.console.modules.diagnostic import main |
| 355 |
main() |
| 2 |
.../umc/python/diagnostic/de.po | 32 ++++++++++++++++++++-- |
356 |
.../umc/python/diagnostic/de.po | 32 ++++++++++++++++++++-- |
| 3 |
1 file changed, 30 insertions(+), 2 deletions(-) |
357 |
1 file changed, 30 insertions(+), 2 deletions(-) |