![](https://habrastorage.org/webt/xr/4d/ip/xr4dipb_zf-xknbveabx-fj5ifg.jpeg)
サブスクライバーへのIPアドレスの発行を手配するタスクが到着しました。 タスク条件:
- 承認のために別のサーバーを提供することはありません-あなたが管理します;)
- 加入者はDHCP経由でネットワーク設定を受信する必要があります
- ネットワークは多様です。 これはPON機器であり、オプション82が設定された通常のスイッチとポイント付きのWiFiベースです
- データがIPを発行する条件のいずれにも該当しない場合、「ゲスト」ネットワークからIPを発行する必要があります。
良いことから:FreeBSDには「動作」できるサーバーがありますが、「このネットワーク上で」ではなく「遠く」です;)。
素晴らしいMikrotikデバイスもあります。 一般的なネットワーク図は次のようなものです。
![](https://habrastorage.org/webt/xt/eo/lb/xteolbkbho0-dpea6bc7-j7koow.png)
少し考えた後、FreeRadiusサブスクライバーを使用してネットワーク設定を発行することにしました。 原則として、スキームは通常です。MicrotickではDHCPサーバーをオンにし、同じRadiusクライアントでオンにします。 多数のDHCPサーバー-> Radius Client-> Radiusサーバーを構成します。
難しくないようです。 しかし! 悪魔は詳細にあります。 すなわち:
- このスキームに従ってPON OLTが承認されると、ヘッドステーションのMACアドレスに等しいUser-Name、PON Onu MACに等しいAgent-Circuit-Id、および空のパスワードを使用して、要求がFreeRadiusに送信されます。
- オプション82のスイッチで許可すると、サブスクライバのMACデバイスに等しい空のユーザー名と、リレースイッチMACおよびサブスクライバが接続されているポートを含む追加のAgent-Circuit-IdおよびAgent-Remote-Idの追加属性を使用して、FreeRadiusに要求が届きます。
- WiFIポイントを持つ一部の加入者は、PAP-CHAPプロトコルを介して承認されます
- WIFIポイントを持つ一部の加入者は、パスワードなしで、WIFIポイントのMACアドレスに等しいユーザー名で許可されます。
歴史的背景:DHCPのOption 82とは
これらは、たとえばAgent-Circuit-IdフィールドやAgent-Remote-Idフィールドなど、追加情報を転送できるDHCPプロトコルの追加オプションです。 通常、リレースイッチのMACアドレスと、加入者が接続されているポートの送信に使用されます。 PON機器またはWIFI基地局の場合、Agent-Circuit-Idフィールドには有用な情報が含まれていません(サブスクライバーポートはありません)。 この場合、この場合のDHCPの一般的なスキームは次のとおりです。
![](https://habrastorage.org/webt/q2/5v/6b/q25v6bvigliru80rv6pu17c2ofm.png)
ステップごとに、このスキームは次のように機能します。
- 加入者機器は、ネットワーク設定のブロードキャストDHCP要求を作成します
- 加入者機器が直接接続されているデバイス(たとえば、スイッチ、WiFiまたはPON基地局)は、このパケットを「インターセプト」して変更し、追加オプションOption 82とリレーエージェントIPアドレスを導入して、ネットワーク上でさらに転送します。
- DHCPサーバーは要求を受け入れ、応答を形成し、それを中継デバイスに送信します
- 中継デバイスは、応答パケットを加入者ユニットに転送します
したがって、これらすべてが機能しないことは言うまでもありません。もちろん、ネットワーク機器の適切な構成が必要です。
FreeRadiusをインストールする
もちろん、FreeRadiusの構成設定を使用すると、これはすべて実現できますが、それは難しく、明確ではありません...特にNか月後に「すべてが機能する」ようにスヌープする場合。 したがって、PythonでFreeRadiusの承認モジュールを作成することにしました。 MySQLデータベースから認証データを取得します。 いずれにせよ、その構造を説明することは意味がありません。 特に、FreeRadiusのsqlモジュールで提案されている構造を採用し、ログインパスワードに加えて、各サブスクライバーのmacフィールドとportフィールドを追加することで若干変更しました。
そのため、まず最初に、FreeRadiusをインストールします。
cd /usr/ports/net/freeradius3 make config make install clean
設定では、インストール用にマークします。
![](https://habrastorage.org/webt/ny/ct/2j/nyct2jjsqsvzlc2cpjlqkex8z5a.png)
pythonモジュールへのシンボリックリンクを作成します(つまり、有効にします)。
ln -s /usr/local/etc/raddb/mods-available/python /usr/local/etc/raddb/mods-enabled
Python用の追加モジュールをインストールします。
pip install mysql-connector
FreeRadiusのpythonモジュール設定では、python_path変数でモジュール検索パスを指定する必要があります。 たとえば、私はこれを持っています:
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インタープリターを実行してコマンドを入力することで見つけることができます。
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によって実行されるスクリプトは、インポートにリストされているモジュールを検出しません。 さらに、モジュール設定で許可およびアカウンティング機能のコメントを外す必要があります。 たとえば、このモジュールは次のようになります。
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に配置する必要があります。合計3つのスクリプトがあります。
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
コードからわかるように、利用可能なすべての方法を使用して、明らかに既知のサブスクライバーMACアドレスまたはOption 82バンドルによってサブスクライバーを識別しようとします。これがうまくいかない場合は、「ゲスト」ネットワークから使用される最も古いIPアドレスを発行します。 指定された時間にPythonスクリプトの必要な機能が作動するように、サイト対応フォルダーにデフォルトのスクリプトを構成します。 実際、ファイルを次の形式にするだけで十分です。
デフォルト
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 } }
実行して、デバッグログへの影響を確認します。
/usr/local/etc/rc.d/radiusd debug
他に何。 FreeRadiusをセットアップするとき、radclientユーティリティを使用して動作をテストすると便利です。 承認の例:
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
または会計:
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アドレスを持つクライアントを既に持っているか」というチェックすらありません。
私の状況で具体的に機能するための単なる「膝の上の解決策」であり、それ以上のことはありません。 厳密に判断しないでください;)