Сетевые настройки из FreeRadius через DHCP
Прилетела задача наладить выдачу IP адресов абонентам. Условия задачи:
- Отдельного сервера под авторизации не дадим — обойдетесь 😉
- Абоненты должны получать сетевые настройки по DHCP
- Сеть разнородная. Это и PON оборудование, и обычные свичи с настроенной Опцией 82 и WiFi базы с точками
- Если ни под одно из условий выдачи IP данные не попадают — необходимо выдать IP из «гостевой» сети
Из хорошего: есть таки сервер на FreeBSD, который может «поработать», но он «за тридевять земель» ;), не «прям в этой сети». Ещё есть устройство Mikrotik. Общая схема сети примерно такая:
Чуть поразмышляв, было принято решение использовать для выдачи сетевых настроек абонентам FreeRadius. В принципе схема обычная: на Microtick включаем DHCP сервер, на нем-же Radius Client. Настраиваем связку DHCP server -> Radius Client -> Radius server.
Вроде бы не сложно. Но! Дьявол кроется в деталях. А именно:
- При авторизации PON OLT по этой схеме на FreeRadius «прилетает» запрос с User-Name равному МАС адресу головной станции, Agent-Circuit-Id равному МАС PON Onu и пустым паролем.
- При авторизации со свичей с опцией 82, на FreeRadius приходит запрос с пустым User-Name равному МАС устройства абонента и заполнеными дополнительными атрибутами Agent-Circuit-Id и Agent-Remote-Id содержащими соответственно опять же МАС релейного свича и порт к которому подключен абонент.
- Часть абонентов с WiFI точек авторизуются через PAP-CHAP протоколы
- Часть абонентов с WIFI точек авторизируются с User-Name равному МАС адресу WIFI точки, без пароля.
Историческая справка: что такое «Option 82» у DHCP
Это дополнительные опции у протокола DHCP которые позволяют передать дополнительную информацию, например в полях Agent-Circuit-Id и Agent-Remote-Id. Обычно используется для передачи МАС адреса релейного свича и порта к которому подключен абонент. В случае оборудования PON или базовых станций WIFI поле Agent-Circuit-Id полезной информации не несёт (нет порта абонента). При этом общая схема работы DHCP в этом случае следующая:
Пошагово эта схема работает так:
- Абонентское оборудование делает широковещательный DHCP запрос на получение сетевых настроек
- Устройство (например свич, базовая станция WiFi или PON) к которому непосредственно подключается абонентское оборудование «перехватывает» этот пакет и изменяет его, внедряя в него дополнительные опции Option 82 и Relay agent IP address,и передает его далее по сети.
- DHCP сервер принимает запрос, формирует ответ и отправляет его релейному устройству
- Релейное устройство переправляет пакет ответа на абонентское устройство
Так просто всё это конечно не работает, нужна соответствующая настройка сетевого оборудования.
Установка FreeRadius
Настройками конфигурации FreeRadius этого конечно достичь всего можно, но сложно и не понятно…особенно когда сунешься туда через N месяцев «всё работает». Потому было принято решение написать свой модуль авторизации для FreeRadius на Python. Данные для авторизации будем брать из базы MySQL. Структуру её описывать смысла нет, всё равно каждый будет её делать «под себя». В частности я взял структуру которая предлагается с модулем sql для FreeRadius, и чуть изменил, добавив поле mac и port для каждого абонента, помимо логина-пароля.
Итак, для начала устанавливаем FreeRadius:
1 2 3 4 |
cd /usr/ports/net/freeradius3 make config make install clean |
В настройках отмечаем для установки:
Делаем симлинк на модуль python (т.е. «включаем» его):
1 |
ln -s /usr/local/etc/raddb/mods-available/python /usr/local/etc/raddb/mods-enabled |
Установим для python дополнительный модуль:
1 |
pip install mysql-connector |
В настройках модуля python для FreeRadius, нужно прописать пути поиска модулей в переменную python_path. Например у меня это:
1 |
python_path="/usr/local/etc/raddb/mods-config/python:/usr/local/lib/python2.7:/usr/local/lib/python27.zip:/usr/local/lib/python2.7:/usr/local/lib/python2.7/plat-freebsd12:/usr/local/lib/python2.7/lib-tk:/usr/local/lib/python2.7/lib-old:/usr/local/lib/python2.7/lib-dynload:/usr/local/lib/python2.7/site-packages" |
Пути можно узнать запустив интерпретатор python и введя команды:
1 2 3 4 5 6 7 8 |
root@phaeton:/usr/local/etc/raddb/mods-enabled# python Python 2.7.15 (default, Dec 8 2018, 01:22:25) [GCC 4.2.1 Compatible FreeBSD Clang 6.0.1 (tags/RELEASE_601/final 335540)] on freebsd12 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.path ['', '/usr/local/lib/python27.zip', '/usr/local/lib/python2.7', '/usr/local/lib/python2.7/plat-freebsd12', '/usr/local/lib/python2.7/lib-tk', '/usr/local/lib/python2.7/lib-old', '/usr/local/lib/python2.7/lib-dynload', '/usr/local/lib/python2.7/site-packages'] > |
Если не сделать этот шаг, то скрипты написанные на python и запущенные FreeRadius не найдут те модули, которые перечислены в import. Кроме того, необходимо раскоментировать в настройках модуля функции вызова авторизации и аккаунтинга. Например у меня выглядит данный модуль так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
python { python_path="/usr/local/etc/raddb/mods-config/python:/usr/local/lib/python2.7:/usr/local/lib/python2.7/site-packages:/usr/local/lib/python27.zip:/usr/local/lib/python2.7:/usr/local/lib/python2.7/plat-freebsd12:/usr/local/lib/python2.7/lib-tk:/usr/local/lib/python2.7/lib-old:/usr/local/lib/python2.7/lib-dynload:/usr/local/lib/python2.7/site-packages" module = work mod_instantiate = ${.module} mod_detach = ${.module} mod_authorize = ${.module} func_authorize = authorize mod_authenticate = ${.module} func_authenticate = authenticate mod_preacct = ${.module} func_preacct = preacct mod_accounting = ${.module} func_accounting = accounting mod_checksimul = ${.module} mod_pre_proxy = ${.module} mod_post_proxy = ${.module} mod_post_auth = ${.module} mod_recv_coa = ${.module} mod_send_coa = ${.module} } |
Скрипт work.py (и все остальные) необходимо положить в /usr/local/etc/raddb/mods-config/python Всего скриптов у меня вышло три.
work.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
#!/usr/local/bin/python # coding=utf-8 import radiusd import func import sys from pprint import pprint mysql_host="localhost" mysql_username="укацук" mysql_password="ыукаыукаыук" mysql_base="ыукаыкуаыу" def instantiate(p): print ("*** instantiate ***") print (p) # return 0 for success or -1 for failure def authenticate(p): print ("*** Аутенфикация!!***") print (p) def authorize(p): radiusd.radlog(radiusd.L_INFO, '*** radlog call in authorize ***') conn=func.GetConnectionMysql(mysql_host, mysql_username, mysql_password, mysql_base); param=func.ConvertArrayToNames(p); pprint(param) print ("*** Авторизация ***") reply = () conf = () cnt=0 username="";mac=""; # сначала проверяем "как положено", по связке логин/пароль if ("User-Name" in param) and ("User-Password" in param) : print ("Вариант авторизации (1): есть логин-пароль") pprint(param["User-Name"]) pprint(param["User-Password"]) pprint(conn) print(sys.version_info) print (radiusd.config) sql="select radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where radcheck.username=%s and radcheck.value=%s" print(sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql,[param["User-Name"], param["User-Password"]]); row = cursor.fetchone() while row is not None: cnt=cnt+1 username=row["username"] reply = reply+((str(row["attribute"]),str(row["value"])), ) row = cursor.fetchone() # вариант, что User-Name - это МАС адрес БС,пароля и порта нет if ("User-Name" in param) and ("User-Password" in param) and (cnt==0): if param["User-Password"] =='': if ":" in param["User-Name"]: pprint(param["User-Name"]) print ("Вариант авторизации (2): User-Name - это MAC адрес базовой станции, порта и пароля нет") sql="select radreply.username,radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where REPLACE(radcheck.mac,':','') = REPLACE(REPLACE('"+str(param["User-Name"])+"','0x',''),':','') and radcheck.sw_port=''" print (sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql); row = cursor.fetchone() while row is not None: cnt=cnt+1 username=row["username"] mac=param["User-Name"] reply = reply+((str(row["attribute"]),str(row["value"])), ) row = cursor.fetchone() if ("Agent-Remote-Id" in param) and ("User-Password" in param) and (cnt==0): if param["User-Password"] =='': pprint(param["Agent-Remote-Id"]) print ("Вариант авторизации (2.5): Agent-Remote-Id - это MAC адрес PON оборудования") sql="select radreply.username,radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where REPLACE(radcheck.mac,':','') = REPLACE(REPLACE('"+str(param["Agent-Remote-Id"])+"','0x',''),':','') and radcheck.sw_port=''" print (sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql); row = cursor.fetchone() while row is not None: cnt=cnt+1 username=row["username"] mac=param["User-Name"] reply = reply+((str(row["attribute"]),str(row["value"])), ) row = cursor.fetchone() #Вариант, что Agent-Remote-Id - это МАС адрес БС,пароля и порта нет и предыдущие варианты поиска IP результата не дали if ("Agent-Remote-Id" in param) and ("User-Password" not in param) and (cnt==0): pprint(param["Agent-Remote-Id"]) print ("Вариант авторизации (3): Agent-Remote-Id - МАС базовой станции/пон. Порта в биллинге нет") sql="select radreply.username,radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where REPLACE(radcheck.mac,':','') = REPLACE(REPLACE('"+str(param["Agent-Remote-Id"])+"','0x',''),':','') and radcheck.sw_port=''" print(sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql); row = cursor.fetchone() while row is not None: cnt=cnt+1 mac=param["Agent-Remote-Id"] username=row["username"] reply = reply+((str(row["attribute"]),str(row["value"])), ) row = cursor.fetchone() #Вариант, что предыдущие попытки результата не дали, но есть Agent-Remote-Id и Agent-Circuit-Id if ("Agent-Remote-Id" in param) and ("Agent-Circuit-Id" in param) and (cnt==0): pprint(param["Agent-Remote-Id"]) pprint(param["Agent-Circuit-Id"]) print ("Вариант авторизации (4): авторизация по Agent-Remote-Id и Agent-Circuit-Id, в биллинге есть порт/мак") sql="select radreply.username,radreply.attribute,radreply.value from radcheck inner join radreply on radreply.username=radcheck.username where upper(radcheck.sw_mac)=upper(REPLACE('"+str(param["Agent-Remote-Id"])+"','0x','')) and upper(radcheck.sw_port)=upper(RIGHT('"+str(param["Agent-Circuit-Id"])+"',2)) and radcheck.sw_port<>''" print(sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql); row = cursor.fetchone() while row is not None: cnt=cnt+1 mac=param["Agent-Remote-Id"] username=row["username"] reply = reply+((str(row["attribute"]),str(row["value"])), ) row = cursor.fetchone() # если так до сих пор IP не получен, то выдаю иего из гостевой сети.. if cnt==0: print ("Ни один из вариантов авторизации не сработал, получаю IP из гостевой сети..") ip=func.GetGuestNet(conn) if ip!="": cnt=cnt+1; reply = reply+(("Framed-IP-Address",str(ip)), ) # если совсем всё плохо, то Reject if cnt==0: conf = ( ("Auth-Type", "Reject"), ) else: #если авторизация успешная (есть такой абонент), то запишем историю авторизации if username!="": func.InsertToHistory(conn,username,mac, reply); conf = ( ("Auth-Type", "Accept"), ) pprint (reply) conn=None; return radiusd.RLM_MODULE_OK, reply, conf def preacct(p): print ("*** preacct ***") print (p) return radiusd.RLM_MODULE_OK def accounting(p): print ("*** Аккаунтинг ***") radiusd.radlog(radiusd.L_INFO, '*** radlog call in accounting (0) ***') print (p) conn=func.GetConnectionMysql(mysql_host, mysql_username, mysql_password, mysql_base); param=func.ConvertArrayToNames(p); pprint(param) print("Удалим старые сессии (более 20 минут нет аккаунтинга)"); sql="delete from radacct where TIMESTAMPDIFF(minute,acctupdatetime,now())>20" cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql); conn.commit() print("Обновим/добавим информацию о сессии") if (("Acct-Unique-Session-Id" in param) and ("User-Name" in param) and ("Framed-IP-Address" in param)): sql='insert into radacct (radacctid,acctuniqueid,username,framedipaddress,acctstarttime) values (null,"'+str(param['Acct-Unique-Session-Id'])+'","'+str(param['User-Name'])+'","'+str(param['Framed-IP-Address'])+'",now()) ON DUPLICATE KEY update acctupdatetime=now()' print(sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql) conn.commit() conn=None; return radiusd.RLM_MODULE_OK def pre_proxy(p): print ("*** pre_proxy ***") print (p) return radiusd.RLM_MODULE_OK def post_proxy(p): print ("*** post_proxy ***") print (p) return radiusd.RLM_MODULE_OK def post_auth(p): print ("*** post_auth ***") print (p) return radiusd.RLM_MODULE_OK def recv_coa(p): print ("*** recv_coa ***") print (p) return radiusd.RLM_MODULE_OK def send_coa(p): print ("*** send_coa ***") print (p) return radiusd.RLM_MODULE_OK def detach(): print ("*** На этом всё детишечки ***") return radiusd.RLM_MODULE_OK |
func.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#!/usr/bin/python2.7 # coding=utf-8 import mysql.connector from mysql.connector import Error # Функция возвращает соединение с MySQL def GetConnectionMysql(mysql_host, mysql_username, mysql_password, mysql_base): try: conn = mysql.connector.connect(host=mysql_host,database=mysql_base,user=mysql_username,password=mysql_password) if conn.is_connected(): print('---cоединение с БД '+mysql_base+' установлено') except Error as e: print("Ошибка: ",e); exit(1); return conn def ConvertArrayToNames(p): mass={}; for z in p: mass[z[0]]=z[1] return mass # Функция записывает историю соединения по известным данным def InsertToHistory(conn,username,mac, reply): print("--записываю для истории") repl=ConvertArrayToNames(reply) if "Framed-IP-Address" in repl: sql='insert into radpostauth (username,reply,authdate,ip,mac,session_id,comment) values ("'+username+'","Access-Accept",now(),"'+str(repl["Framed-IP-Address"])+'","'+str(mac)+'","","")' print(sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql); conn.commit() # Функция выдает последний по дате выдачи IP адрес из гостевой сети def GetGuestNet(conn): ip="";id=0 sql="select * from guestnet order by dt limit 1" print (sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql); row = cursor.fetchone() while row is not None: ip=row["ip"] id=row["id"] row = cursor.fetchone() if id>0: sql="update guestnet set dt=now() where id="+str(id) print (sql) cursor = conn.cursor(dictionary=True,buffered=True) cursor.execute(sql); conn.commit() return ip |
radiusd.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#!/usr/bin/python2.7 # coding=utf-8 # from modules.h RLM_MODULE_REJECT = 0 RLM_MODULE_FAIL = 1 RLM_MODULE_OK = 2 RLM_MODULE_HANDLED = 3 RLM_MODULE_INVALID = 4 RLM_MODULE_USERLOCK = 5 RLM_MODULE_NOTFOUND = 6 RLM_MODULE_NOOP = 7 RLM_MODULE_UPDATED = 8 RLM_MODULE_NUMCODES = 9 # from log.h L_AUTH = 2 L_INFO = 3 L_ERR = 4 L_WARN = 5 L_PROXY = 6 L_ACCT = 7 L_DBG = 16 L_DBG_WARN = 17 L_DBG_ERR = 18 L_DBG_WARN_REQ = 19 L_DBG_ERR_REQ = 20 # log function def radlog(level, msg): import sys sys.stdout.write(msg + '\n') level = level |
Как видно по коду, мы всеми доступными способами пытаемся идентифицировать абонента по его заведомо известным абонентским MAC адресам или связке Option 82, и если это не получается, то выдаем самый старый из использованных когда либо IP адресов из «гостевой» сети. Осталось настроить скрипт default в папке sites-enabled, для того чтобы нужные фукции из скрипта на python дергались в обозначенные моменты. Фактически достаточно файл привести к виду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
server default { listen { type = auth ipaddr = * port = 0 limit { max_connections = 16 lifetime = 0 idle_timeout = 30 } } listen { ipaddr = * port = 0 type = acct limit { } } listen { type = auth port = 0 limit { max_connections = 1600 lifetime = 0 idle_timeout = 30 } } listen { ipv6addr = :: port = 0 type = acct limit { } } authorize { python filter_username preprocess expiration logintime } authenticate { Auth-Type PAP { pap python } Auth-Type CHAP { chap python } Auth-Type MS-CHAP { mschap python } eap } preacct { preprocess acct_unique suffix files } accounting { python exec attr_filter.accounting_response } session { } post-auth { update { &reply: += &session-state: } exec remove_reply_message_if_eap Post-Auth-Type REJECT { attr_filter.access_reject eap remove_reply_message_if_eap } Post-Auth-Type Challenge { } } pre-proxy { } post-proxy { eap } } |
Пробуем запустить и посмотреть что прилетает в отладочный лог:
1 |
/usr/local/etc/rc.d/radiusd debug |
Что еще. При настройке FreeRadius удобно тестировать его работу при помощи утилиты radclient. Например авторизация:
1 |
echo "User-Name=4C:5E:0C:2E:7F:15,Agent-Remote-Id=0x9845623a8c98,Agent-Circuit-Id=0x00010006" | radclient -x 127.0.0.1:1812 auth testing123 |
Или аккаунтинг:
1 |
echo "User-Name=4C:5E:0C:2E:7F:15,Agent-Remote-Id=0x00030f26054a,Agent-Circuit-Id=0x00010002" | radclient -x 127.0.0.1:1813 acct testing123 |
Хочу предупредить, что применять подобную схему и скрипты «без изменений» в «промышленных» масштабах ну никак нельзя. Как минимум бросаются в глаза:
- возможна «подделка» MAC адреса. Достаточно абоненту прописать себе чужой MAC и будут проблемы
- логика выдачи гостевых сетей ниже всякой критики. Нет даже проверки «у может уже есть клиенты с выданным таким IP адресом?»
Это просто «решение на коленке», для того чтобы работало конкретно в моих условиях, не более того. Не судите строго 😉