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