diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py new file mode 100644 index 00000000..e6e612d3 --- /dev/null +++ b/freeradius_utils/auth.py @@ -0,0 +1,262 @@ +# ⁻*- mode: python; coding: utf-8 -*- +""" +Backend python pour freeradius. + +Ce fichier contient la définition de plusieurs fonctions d'interface à +freeradius qui peuvent être appelées (suivant les configurations) à certains +moment de l'authentification, en WiFi, filaire, ou par les NAS eux-mêmes. + +Inspirés d'autres exemples trouvés ici : +https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/ +""" + +import logging +import netaddr +import radiusd # Module magique freeradius (radiusd.py is dummy) +import os +import binascii +import hashlib +import subprocess + +import os, sys +from ast import literal_eval as make_tuple + +#: Serveur radius de test (pas la prod) +TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) + + +## -*- Logging -*- + +class RadiusdHandler(logging.Handler): + """Handler de logs pour freeradius""" + + def emit(self, record): + """Process un message de log, en convertissant les niveaux""" + if record.levelno >= logging.WARN: + rad_sig = radiusd.L_ERR + elif record.levelno >= logging.INFO: + rad_sig = radiusd.L_INFO + else: + rad_sig = radiusd.L_DBG + radiusd.radlog(rad_sig, record.msg) + +# Initialisation d'un logger (pour logguer unifié) +logger = logging.getLogger('auth.py') +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(name)s: [%(levelname)s] %(message)s') +handler = RadiusdHandler() +handler.setFormatter(formatter) +logger.addHandler(handler) + +def radius_event(fun): + """Décorateur pour les fonctions d'interfaces avec radius. + Une telle fonction prend un uniquement argument, qui est une liste de tuples + (clé, valeur) et renvoie un triplet dont les composantes sont : + * le code de retour (voir radiusd.RLM_MODULE_* ) + * un tuple de couples (clé, valeur) pour les valeurs de réponse (accès ok + et autres trucs du genre) + * un tuple de couples (clé, valeur) pour les valeurs internes à mettre à + jour (mot de passe par exemple) + + On se contente avec ce décorateur (pour l'instant) de convertir la liste de + tuples en entrée en un dictionnaire.""" + + def new_f(auth_data): + if type(auth_data) == dict: + data = auth_data + else: + data = dict() + for (key, value) in auth_data or []: + # Beware: les valeurs scalaires sont entre guillemets + # Ex: Calling-Station-Id: "une_adresse_mac" + data[key] = value.replace('"', '') + try: + # TODO s'assurer ici que les tuples renvoyés sont bien des (str,str) + # rlm_python ne digère PAS les unicodes + return fun(data) + except Exception as err: + logger.error('Failed %r on data %r' % (err, auth_data)) + raise + + return new_f + + + +@radius_event +def instantiate(*_): + """Utile pour initialiser les connexions ldap une première fois (otherwise, + do nothing)""" + logger.info('Instantiation') + if TEST_SERVER: + logger.info('DBG_FREERADIUS is enabled') + +@radius_event +def authorize(data): + """Fonction qui aiguille entre nas, wifi et filaire pour authorize + On se contecte de faire une verification basique de ce que contien la requète + pour déterminer la fonction à utiliser""" + if data.get('NAS-Port-Type', '')==u'Ethernet': + return authorize_fil(data) + elif u"Wireless" in data.get('NAS-Port-Type', ''): + return authorize_wifi(data) + +@radius_event +def authorize_wifi(data): + """Section authorize pour le wifi + (NB: le filaire est en accept pour tout le monde) + Éxécuté avant l'authentification proprement dite. On peut ainsi remplir les + champs login et mot de passe qui serviront ensuite à l'authentification + (MschapV2/PEAP ou MschapV2/TTLS)""" + + items = get_machines(data) + + if not items: + logger.error('Nothing found') + return radiusd.RLM_MODULE_NOTFOUND + + if len(items) > 1: + logger.warn('lc_ldap: Too many results (taking first)') + + machine = items[0] + + proprio = machine.proprio() + if isinstance(proprio, lc_ldap.objets.AssociationCrans): + logger.error('Crans machine trying to authenticate !') + return radiusd.RLM_MODULE_INVALID + + for bl in machine.blacklist_actif(): + if bl.value['type'] in BL_REJECT: + return radiusd.RLM_MODULE_REJECT + # Kludge : vlan isolement pas possible, donc reject quand-même + if not WIFI_DYN_VLAN and bl.value['type'] in BL_ISOLEMENT: + return radiusd.RLM_MODULE_REJECT + + + if not machine.get('ipsec', False): + logger.error('WiFi auth but machine has no password') + return radiusd.RLM_MODULE_REJECT + + password = machine['ipsec'][0].value.encode('ascii', 'ignore') + + # TODO: feed cert here + return (radiusd.RLM_MODULE_UPDATED, + (), + ( + ("Cleartext-Password", password), + ), + ) + +@radius_event +def authorize_fil(data): + """ + Check le challenge chap, et accepte. + """ + + chap_ok = False + # Teste l'authentification chap fournie + # password et challenge doivent être données + # en hexa (avec ou sans le 0x devant) + # le User-Name est en réalité la mac ( xx:xx:xx:xx:xx ) + password = data.get('CHAP-Password', '') + challenge = data.get('CHAP-Challenge', '') + mac = data.get('User-Name', '') + + logger.debug('(fil) authorize(%r)' % ((password, challenge, mac),)) + + try: + challenge = binascii.a2b_hex(challenge.replace('0x','')) + password = binascii.a2b_hex(password.replace('0x','')) + if hashlib.md5(password[0] + mac + challenge).digest() == password[1:]: + logger.info("(fil) Chap ok") + chap_ok = True + else: + logger.info("(fil) Chap wrong") + except Exception as err: + logger.info("(fil) Chap challenge check failed with %r" % err) + + if not chap_ok: + if TEST_SERVER: + logger.debug('(fil) Continue auth (debug)') + else: + return radiusd.RLM_MODULE_REJECT + + return (radiusd.RLM_MODULE_UPDATED, + (), + ( + ("Auth-Type", "Accept"), + ), + ) + +@radius_event +def post_auth(data): + # On cherche quel est le type de machine, et quel sites lui appliquer + if data.get('NAS-Port-Type', '')==u'Ethernet': + return post_auth_fil(data) + elif u"Wireless" in data.get('NAS-Port-Type', ''): + return post_auth_wifi(data) + +@radius_event +def post_auth_wifi(data): + """Appelé une fois que l'authentification est ok. + On peut rajouter quelques éléments dans la réponse radius ici. + Comme par exemple le vlan sur lequel placer le client""" + + port, vlan_name, reason = decide_vlan(data, True) + mac = data.get('Calling-Station-Id', None) + + log_message = '(wifi) %s -> %s [%s%s]' % \ + (port, mac, vlan_name, (reason and u': ' + reason).encode('utf-8')) + logger.info(log_message) + + # Si NAS ayant des mapping particuliers, à signaler ici + vlan_id = config.vlans[vlan_name] + + # WiFi : Pour l'instant, on ne met pas d'infos de vlans dans la réponse + # les bornes wifi ont du mal avec cela + if WIFI_DYN_VLAN: + return (radiusd.RLM_MODULE_UPDATED, + ( + ("Tunnel-Type", "VLAN"), + ("Tunnel-Medium-Type", "IEEE-802"), + ("Tunnel-Private-Group-Id", '%d' % vlan_id), + ), + () + ) + + return radiusd.RLM_MODULE_OK + +@radius_event +def post_auth_fil(data): + """Idem, mais en filaire. + """ + + nas = data.get('NAS-Identifier', None) + port = data.get('NAS-Port', None) + mac = data.get('Calling-Station-Id', None) + out = subprocess.check_output(['/usr/bin/python3', '/var/www/re2o/freeradius_utils/authenticate_filaire.py', nas, port, mac]) + reason, vlan_id = make_tuple(out) + + log_message = '(fil) %s -> %s [%s%s]' % \ + (nas + u":" + port, mac, vlan_id, (reason and u': ' + reason).encode('utf-8')) + logger.info(log_message) + + # Filaire + return (radiusd.RLM_MODULE_UPDATED, + ( + ("Tunnel-Type", "VLAN"), + ("Tunnel-Medium-Type", "IEEE-802"), + ("Tunnel-Private-Group-Id", '%d' % int(vlan_id)), + ), + () + ) + +@radius_event +def dummy_fun(_): + """Do nothing, successfully. (C'est pour avoir un truc à mettre)""" + return radiusd.RLM_MODULE_OK + +def detach(_=None): + """Appelé lors du déchargement du module (enfin, normalement)""" + print "*** goodbye from auth.py ***" + return radiusd.RLM_MODULE_OK + diff --git a/freeradius_utils/modules/rlm_python_re2o.conf b/freeradius_utils/modules/rlm_python_re2o.conf new file mode 120000 index 00000000..5bd5d2ad --- /dev/null +++ b/freeradius_utils/modules/rlm_python_re2o.conf @@ -0,0 +1 @@ +../rlm_python_re2o.conf \ No newline at end of file