
Tuli ülesanne korraldada IP-aadresside jagamine abonentidele. Ülesande tingimused:
- Erinevat serverit autentimiseks ei paku — peate hakkama saama 😉
- Abonentidel peavad olema võrgu seaded DHCP kaudu
- Võrk on mitmekesine. Seal on PON-seade, tavalised lülitid koos seadistatud valikuga 82 ja WiFi baasid punktidega
- Kui ükski IP jagamise tingimustest ei sobi, tuleb välja anda IP "külalisvõrgu" hulgast
Hea uudis: on olemas server FreeBSD, mis saab "tööd teha", kuid see on "üheksas kuningriigis" ;), mitte "täpselt sellel võrgul".
On veel suurepärane seade Mikrotik. Üldine skeem võrgustik on umbes selline:

Veidi mõlgutades tehti otsus kasutada abonentide võrgu seadete jagamiseks FreeRadius. Põhimõtteliselt on skeem tavaline: Microtickil lülitame sisse DHCP serveri, samuti Radius Clienti. Seame üles sideme DHCP server -> Radius Client -> Radius server.
Ei tundu keeruline. Aga! Kurat peitub detailides. Täpsemalt:
- PON OLT autoriseerimisel selle skeemi kaudu FreeRadius "tuleb" päring, mille User-Name on võrgu põhijaama MAC-aadress, Agent-Circuit-Id võrgu kuidas MAC PON Onu ja tühi parool.
- SWITCH-ide kaudu autentimisel koos valikuga 82 käib FreeRadiusesse päring, kus User-Name on tühi ja vastab kasutaja seadme MAC-aadressile, ning täiendavad atribuudid Agent-Circuit-Id ja Agent-Remote-Id on täidetud, sisaldades vastavalt samuti rikka switch'i MAC-aadressi ja porti, millega kasutaja on ühendatud.
- Osa kasutajatest WiFi punktide kaudu autentib end PAP-CHAP protokollide kaudu.
- Osa WiFi punktide kasutajatest autentib end User-Name'iga, mis vastab WiFi punkti MAC-aadressile, ilma paroolita.
Ajalooline teave: mis on DHCP-s "Option 82"?
Need on täiendavad valikud DHCP protokollis, mis võimaldavad edastada täiendavat teavet, näiteks Agent-Circuit-Id ja Agent-Remote-Id väljades. T tavaliselt kasutatakse rikka switch'i MAC-aadressi ja porti, millega kasutaja on ühendatud, edastamiseks. PON-seadmete või WiFi baasjaamade puhul ei oma Agent-Circuit-Id väli kasulikku teavet (kasutaja porti pole). Sellegipoolest on DHCP üldine tööschema sellisel juhul järgmine:

Samm-sammult toimib see skeem järgmiselt:
- Kasutaja seadmed saadavad laialdase DHCP-päringu võrgu seadistuste saamiseks.
- Seade (näiteks lüliti, WiFi baasjaam või PON), millega otse on ühendatud klientseade, "püüdab" selle paketi ja muudab seda, lisades juurde valikud Option 82 ja Relay agent IP address ning edastab selle edasi võrgus.
- DHCP server võtab vastu päringu, vormib vastuse ja saadab selle edastusseadmest.
- Edastusseade suunab vastusepaketi kliendiseadmesse.
Kahjuks ei tööta see nii lihtsalt, vajalik on vastava võrgu seadmete seadistamine.
FreeRadiusi paigaldamine
FreeRadiusi konfiguratsioonisätete abil on seda kõike küll võimalik saavutada, kuid see on keeruline ja arusaamatu... eriti kui sinna sukelduda pärast N kuud, mil "kõik töötab". Seetõttu otsustati kirjutada oma autentimise moodul FreeRadiusile Pythonis. Autentimiseks vajalikud andmed võetakse MySQL andmebaasist. Selle struktuuri kirjeldamine pole mõtet, sest igaüks teeb selle ikkagi "enda jaoks". Konkreetselt kasutasin struktuuri, mis on ette nähtud FreeRadiusile sql mooduli jaoks, ja muutsin seda pisut, lisades iga abonendi jaoks, peale kasutajanime ja parooli, välja mac ja port.
Kuna alustame, paigaldame FreeRadiusi:
cd /usr/ports/net/freeradius3
make config
make
install clean
Seadistustes valime installimiseks:

Teeme sümbolsid python mooduli jaoks (st „aktiveerime” selle):
ln -s /usr/local/etc/raddb/mods-available/python /usr/local/etc/raddb/mods-enabledInstallime pythonile lisamooduli:
pip install mysql-connectorFreeRadiuse python mooduli seadetes tuleb module otsinguteed kirjutada muutujasse python_path. Näiteks on mul see:
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"
Teid saab teada, kui käivitate python interpreteri ja sisestate käsud:
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)] freebsd12 süsteemis
Tüüp "help", "copyright", "credits" või "license" rohkem teabe jaoks.
>>> 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']
>Kui seda sammu ei astu, ei leia FreeRadiusel Pythonis kirjutatud skriptid neid mooduleid, mis on importi loetletud. Lisaks tuleb ka mooduli autoriseerimise ja arvestuse konfiguratsioonis vajalikud read välja kommenteerida. Näiteks minu moodul näeb välja selline:
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}
}Skripti work.py (ja kõik ülejäänud) tuleb paigutada /usr/local/etc/raddb/mods-config/python. Kokku on mul kolme skripti.
work.py:
#!/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:
#!/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:
#!/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
Nagu koodist nähtus, püüame igal võimalikul viisil identifitseerida abonenti tema teadaolevate MAC-aadresside või Option 82 ühenduse järgi. Kui see ei õnnestu, anname välja kõige vanema IP-aadressi, mida kunagi „külalis“ võrgu raames on kasutatud. Jäänud on konfigureerida skript default kaustas sites-enabled, et vajalikud funktsioonid kutsutaks välja Python'i skriptist määratud aegadel. Tegelikult piisab, kui fail viia sellisesse vormi:
default
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
}
}
Proovime käivitada ja vaatame, mis tuleb tõrke logisse:
/usr/local/etc/rc.d/radiusd debugVeel üks asi. FreeRadiuse seadistamisel on mugav testida selle tööd rakendusega radclient. Näiteks autoriseerimine:
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 testing123Või akounting:
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 testing123Soovin hoiatada, et selliste skeemide ja skriptide kasutamine "ilma muudatusteta" "tööstuslikul" tasemel ei ole kuidagi lubatud. Vähemalt on näha:
- MAC aadressi "võltsimine" on võimalik. Piisab, kui abonent määrab endale vale MAC-i ja probleemid on kohal
- Külastusvõrkude väljaandmise loogika on allpool igasugust kriitikat. Siin pole isegi kontrolli: "kas sellise IP aadressiga kliendid on juba olemas?"
See on lihtsalt "kätega lahendus", et see töötab konkreetselt minu tingimustes, mitte rohkem. Ärge hinnake liiga rangelt 😉
Allikas: habr.com
