محسن نوشته

Serving Configuration در FreeSwitch چیه!
منتشر شده در: — Sep 5, 2019

توی این پست و پست بعدی درباره کاربرد FreeSwitch در محصولات بزرگ مقیاس با هم انتقال تجربه داریم. من VoIP و مفاهیم مرتبط را با Asterisk یاد گرفتم و مدتها فکر میکردم یه سولوشن بی نظیر برای این کار هستش! این تفکر هم به این خاطر ایجاد شده بود که همیشه باهاش میتونستم نیازمندی های سازمانها را رفع کنم. کار بجایی رسید که وقتی دانشجوی ارشد بودم شروع به تدریسش کردم! غافل از اینکه این تفکر اونقدرها هم نمیتونه اعتبار داشته باشه. وقتی با مجموعه بیسفون و خاصه برای مباحث VoIP با تیم بیستاک وارد همکاری شدم تازه فهمیدم VoIP یعنی چی! خوب مجموعه بیستاک برند 020 را داره و مدتها لیدر بازار VoIP بوده و قطعا میتونست بلوغ منو توی این تکنولوژی ارتقا بده، که اتفاقا همینطور هم شد و من اونجا با FreeSwitch آشنا شدم و تازه درکم نسبت به VoIP ارتقای خوبی پیدا کرد.

اگه بخوام اول درباره تفاوت های FreeSwitch و Asterisk بگم مهمترین این تفاوت ها اینه که Asterisk خودش یک محصول یکپارچه و کامل هستش. همین تکاملش از یک طرف کمک میکنه تکنسین ها و دولوپرهای معمولی باهاش راحت باشن و توی اسکیل سازمانهای کوچک و متوسط به راحتی مطلوبات را ارضا کنه و از طرف دیگه تجمع همه این قابلیت ها یکجا، Asterisk را تبدیل به یک محصول سنگین وزن و چاق کرده! اما FreeSwitch دقیقا نقطه مقابل Asterisk از این نگاه هستش. ما برای کار کردن باهاش به تسلط بیشتری به مفاهیم VoIP و برنامه نویسی نیاز داریم. علاوه بر این FreeSwitch میتونه خیلی سبک وزن به معماری محصول بزرگ مقیاس ما اضافه شه و بی دردسر با پایداری وحشتناک کار کنه.

اولش من می خواستم توی یک پست دو تا تجربه مهم برای استفاده FreeSwitch در محصولات بزرگ مقیاس را به اشتراک بزارم. اما بعدش دیدم اندازه پست خیلی بزرگ میشه و این میتونه خسته کننده باشه. بهمین خاطر تصمیمم عوض شد و دو تا پستش کردم. توی این پست درباره Serving Configuration صحبت می کنیم و در پست بعدی درباره معماری سکوی پردازش توزیع شده مناسب برای VoIP در سیستم های بزرگ مقیاس صحبت خواهیم کرد.

خوب باز هم رویکردمون از بالا به پایین هست. بهمین خاطر بریم سراغ اینکه ببینیم اول داستان چیه. برای اینکه بتونیم بین دو Call-Leg تماس ایجاد کنیم UA های هر Leg باید ابتدا روی IPPBX رجیستر شده باشن. حالا فرض کنیم پیام رسانی بخواد از IPPBXها برای تماس صوتی استفاده کنه. بنابراین وقتی ما قصد داریم با استفاده از پیام رسانمون با مخاطبی تماس تلفنی داشته باشیم، هم پیام رسان ما و هم پیام رسان مخاطب باید ابتدی روی IPPBX رجیستر شده باشند. برای ارضا این نیازمندی لازمه اطلاعات لازم برای اینکار در اختیار IPPBX قرار داشته باشه یا درواقع اکانت SIP هر دو کاربر روی IPPBX تعریف شده باشه. یه راه ساده اینه که وقتی کاربر، پیام رسان نصب می کنه، توی روال نصب اولیه براش یه دونه اکانت SIP ساخته بشه تا هر وقت میخوان روی IPPBX رجیستر کنن از این اکانت استفاده کنن. روش خوبیه اما هم پیچیدگی داره هم اینکه بقول ‘ممد هکر’ براحتی زده میشه! نمیخوام درگیر جزئیات بشم فقط یه گیر تابلو ماجرا اینجاست که تعریف یک اکانت ثابت SIP برای هر کاربر پیام رسان خطر داره! اولین خطرش اینه که اگه من اطلاعات این اکانت را بردارم میتونم با یه دونه SIP-Phone به جای کاربر به هر مخاطبش زنگ بزنم و مثلا فحش بدم! البته من اینکار را نمی کنم! حالا اگه پیامرسان فقط تماس داخل برنامه داشته باشه ضرر مالی متوجه خودش و کاربرش نمیشه. اما اگه مثل بیسفون تماس خارج برنامه داشته باشه داستان بدتر میشه. چرا که میتونم با این اکانت لو رفته شماره همراه دوستمو توی سوئد بگیرم و باهاش مجانی صحبت کنم! درواقع میتونم Replay Attack روی IPPBX داشته باشیم.

حالا میشه به روش بالا کلی وصله زد تا بتونیم مثلا درد ماجرا را کمتر کنیم اما میشه بیشتر فکر کنیم و راه حل های بهتری ارائه کنیم. بعنوان مثال یه سولوشن جالب اینه که برای خودمون درد سر درست نکنیم و هیچ اکانت SIPی برای کاربرامون از قبل نسازیم. بجاش هر وقت کاربر پیامرسان اومد سراغ IPPBX همونجا براش یه اکانت AdHoc بسازیم و کارش که تموم شد اکانتشم پاک شه. این میتونه یه بخش از پیاده سازی و ارتباط بین سرویس ها را کم کنه اما اینجوری اگه باشه که هرکی هرکی میشه و چون هر IPPBX ما ظرفیت محدودی داره یه مشت ‘ممد هکر’ براحتی میتونن این ظرفیت را بگیرن و دیگه هیچکی نتونه تماس داشته باشه. باید دنبال راهی بگردیم تا این اتفاق نیفته و سلوشن پیشنهادی ما، از اضافه کردن اکانت SIP سخت تر نباشه. باز بعنوان مثال میتونیم توی SIP-Header توکن یکبار مصرف حمل کنیم و IPPBX اگه تونست اصالت توکن را احراز کنه اجازه بده کاربر رجیستر کنه. شاید توی ظاهر پیچیده بیاد اما توی این پست با هم تجربه می کنیم که اونقدر هم داستان پیچیده نیست. البته باید دقت داشته باشیم که صرفا استفاده از توکن با این مدل نمیتونه سولوشن کاملی باش و لازمه همراه توکن موارد دیگه ای هم لحاظ بشن اما ما بخاطر پیچیده نشده موضوع اینجا درگیرش نمیشیم. علاوه بر این بخاطر حفظ اسرار شرکت من هم اجازه تشریح این بخش را ندارم. اگه روزی اجازه این کار را داشتم حتما در باره اش می نویسم.

حالا یه بار مرور کنیم! شکل زیر روش معمول احراز کاربر توی IPPBX ها را نشون میده. توی این روش از قبل اکانت های SIP کاربران در اختیار IPPBX قرار می گیره و وقتی UA کاربری بخواد برای انجام تماس REGISTER کنه این اکانت ها مورد استفاده قرار می گیره.

SIP-REGISTER

اما دیدیم که این روش مشکلات امنیتی داره و ما برای رفعش روش AdHoc را ارائه دادیم. شکل زیر میتونه به درک بیشتر ماجرا کمک کنه:

Serving-Configuration

در واقع وقتی UA کاربری میخواد REGISTER کنه، همون لحظه با اجرای یه اسکریپت ساده، براش اکانت SIP ساخته میشه و اینجوری کاربر میتونه به لیست کاربرای Register شده که توی RAM نگهداری میشه، اضافه بشه. کلا به این تکنیک میگیم Serving Configuration و حواسمون باشه که چندین مدل داره. مثلا حتی میشه Dial Plan توی IPPBX تزریق کنیم. واقعیت اینه که اگه IPPBX بتونه اتفاقات (Actions) را کشف کنه میتونه لحظه رخداد Action اسکریپت لینک شده به اون Action را اجرا کنه و تنظیماتشو مطابق چیزی که ما دوست داریم بروز کنه.

توی FreeSwitch ماژول های متعددی برای اجرا کردن انواع اسکریپت ها وجود داره. ما برای بیسفون اسکریپت های مورد نیاز را با LUA می نوشتیم. اما واقعا نصب LUA و Moduleهاش یه کار مزخرفه و خیلی وقت ها نمیشد بخاطر LUA نسخه FreeSwitch را بروز کنیم. برای رفع این مشکل تصمیم گرفتم LUA را کنار بزارم و با Python اسکریپت بنویسم. اینجوری می تونیم آخرین نسخه های FreeSwitch را بدون برخورد Packegeها، استفاده کنیم. برای اینکه کارمون راحت تر بشه یه DockerFile آماده کردم که توش همه ابزارهای مورد نیاز برای این پست قرار داده شده و براحتی میتونید ازش استفاده کنید. علاوه بر این ما برای اجرای سناریوهای VoIP از ابزار SIPp استفاده می کنیم. روی اینترنت متاسفانه مطلب مفید درباره این پست کمه و جاهایی که حس کنم نیاز به شفافیت بیشتری داره، تلاش می کنم توضیح بیشتر داشته باشیم.

خوب کارمون را بهتره شروع کنیم و ببینیم داستان این پست توی FreeSwitch چطوریاست. برای اینکه بتونید Container بسازید باید Docker نصب داشته باشید. دستورات زیر نشون میده چطوری Container مورد نیازمونو آماده کنیم:

1
2
3
4
git clone https://github.com/mohsenmoqadam/freeswitch.git
cd freeswitch
docker build -t freeswitch .
docker run -d freeswitch

خوب اگه مراحل بالا بدون خطا انجام شده Container مورد نظر ما آماده هست و با استفاده از دستور زیر شناسه شو برمیداریم

1
docker container ls

خروجی دستور بالا روی سیستم من مشابه زیر هست:

1
2
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
46e5b89570bf        freeswitch          "supervisord -c /etc…"   9 minutes ago       Up 9 minutes                            jovial_bardeen

اگه دقت کنیم اولین ستون خروجی بالا CONTAINER ID هست و شناسه Container مورد نظر ما 46e5b89570bf هستش. از این شناسه استفاده می کنیم و روی دو تا کنسول مختلف دستور زیر را اجرا می کنیم:

1
docker exec -it 46e5b89570bf bash

حالا ما دو تا کنسول داریم که به Container وصل هستن. از کنسول اول برای مشاهده خط فرمان FreeSwitch استفاده میکنیم. بنابراین دستور زیر را روی کنسول اول اجرا می کنیم:

1
./fs_cli

و از کنسول دوم برای مشاهده فایل های مورد نظر و اجرای سناریو استفاده خواهیم کرد. بهتره روی کنسول دوم دستور زیر را اجرا کنیم:

1
cd /usr/local/sipp/scenarios

خوب حالا همه چی آماده است تا سناریوی رجیستر را با استفاده از SIPp اجرا کنیم. SIPp ابزاری کامل برای اجرای انواع سناریوهای VoIP و استرس تست هستش. توی این پست ما فقط یک سناریو داریم که من این سناریو را توی فایل reg.xml نوشتم. قبل از اینکه ببینیم این فایل چیه بهتره سناریو را اجرا کنیم تا ببینیم چه اتفاقی میفته. برای اجرای سناریو دستور زیر را روی کنسول دوم اجرا میکنیم:

1
sipp 172.17.0.2 -sf reg.xml -inf injection_file -m 1

شکل زیر خروجی این دستور را روی کنسول اول نمایش میده:

FreeSwitch-Console

خطوط سبز رنگ در واقع توسط اسکریپتی چاپ شده اند که موقع رخداد sip_auth توسط FreeSwitch اجرا شده و داره بما گزارش میده یک UA با شماره 1200 روال REGISTER را شروع کرده. دستور show registrations هم لیست کاربرانی که در حال حاضر روی FreeSwitch رجیستر هستند را نمایش میده. چیزی که مشخصه اینه که UA با شناسه 1200 بعد از اجرای سناریو، رجیستر شده.

شکل زیر هم بخشی از خروجی SIPp را روی کنسول دوم نمایش میده:

SIPp-Console

مواردی که با خط قرمز مشخص شده برامون مهمن. بعنوان مثال SIPp اول REGISTER را به سمت FreeSwitch ارسال کرده و منتظر دریافت پیام 100 بوده اما چون ما اینجا Proxy نداریم این پیام دریافت نشده و پیام 401 دریافت شده است. این پیام به UA میفهمونه که اطلاعات احراز هویت لازم هست بنابراین SIPp درخواست رجیستر بعدی را میتونه با این اطلاعات ارسال کنه و همونطور که توی شکل بالا مشخصه وقتی اطلاعات مطلوب ارسال شده پیام 200 دریافت شده و این یعنی اینکه عملیات رجیستر UA با شناسه 1200 با موفقیت انجام شد. حالا این UA میتونه تماس بگیره یا تماس بپذیره!

حالا باید به یک نکته دقت کنیم و اون اینه که ما که هیچ اکانت SIPی برای کاربری با شناسه 1200 از قبل نساختیم پس چطور FreeSwitch این کاربر را با ارسال پیام 200 رجیستر کرد! من FreeSwitch را تنظیم کردم که هر وقت Actionی از جنس sip_auth دریافت کرد قطعه ای از اسکریپت مورد نظرمو اجرا کنه. این اسکریپت یک اکانت SIP برای کاربر با شناسه 1200 ایجاد میکنه و چون درخواست REGISTER بعد از اجرای اسکریپت من پردازش میشه پس این کاربر میتونه REGISTER بشه.

حالا ببینیم این ماجرا چطور انجام میشه. روی کنسول دوم دستور زیر را اجرا کنید:

1
vim /usr/local/freeswitch/conf/autoload_configs/python.conf.xml

این دستور فایل پیکربندی ماژول پایتون را برامون باز می کنه. توی این فایل ما به FreeSwitch گفتیم که چه ماژول پایتونی (pyPack.serve_users) برای چه قسمتی (directory) قراره کانفیگ تزریق کنه. هر وقت FreeSwitch سراغ این ماژول میره تابع xml_fetch این ماژول را فراخوانی می کنه و انتظار داره اطلاعاتی مناسب برای directory دریافت کنه. قطعه کد زیر سورس ماژول pyPack.serve_users را نمایش میده:

 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
from freeswitch import *

def xml_fetch( param1, param2 ):
    try:
        params_serialized = Event.serialize(param1).replace("\"", "")[:-2]
        params_dict = dict(item.split(":") for item in params_serialized.split("\n"))
     
        tvt = params_dict["X-TVT"]
        domain = params_dict["domain"].strip()
        user = params_dict["user"].strip()
        action = params_dict["action"].strip() 

        if action == "sip_auth":
            consoleLog( "info", "===> Action: %s \n" % action )    
            consoleLog( "info", "     User: %s \n" % user )
            consoleLog( "info", "     Domain: %s \n" % domain )
            consoleLog( "info", "     TVT: %s \n" % tvt )

            xml = """ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
                            <document type="freeswitch/xml">
                                <section name="directory">
                                    <domain name="%s">
                                        <user id="%s">
	                                    <params>
	                                        <param name="password" value=""/>
	                                        <param name="vm-password" value=""/>
	                                    </params>
                                        </user>
                                    </domain>
                              </section>
                     </document> """ % (domain, user)
            return xml
        else:
            return """ """

    except:
        return """ """

همیشه FreeSwitch از طریق آرگومان اول تابع xml_fetch اطلاعات مربوط به Action مربوطه را به ما تحویل میده. ما میتونیم از این اطلاعات را مثلا برای اضافه کردن اکانت SIP استفاده کنیم. اگه دقت کنیم یکی از این اطلاعات TVT مخفف Temporary VoIP Token هستش. این توکن موقت یکبار مصرف میتونه شامل اطلاعاتی برای احراز کاربر با شناسه 1200 باشه. بعنوان مثال اگه این توکن با JWT ساخته شده باشه میتونیم امضا و صحت توکن را ابتدا تصدیق کنیم و اگه این تصدیق پاس شد اطلاعات اکانت SIP کاربر را مطابق کد بالا در فراخوانی xml_fetch برگردانیم.

خوب باید یکم درباره موضوع بالا فکر و تحلیل کنید تا اگه هنوز موضوع گنگه براتون شفاف تر بشه. حالا بریم سراغ SIPp و سناریو REGISTER. همونظور که بالا توضیح دادم ما با استفاده از SIPp میتونیم سناریوهای مختلف VoIP را امتحان کنیم. کد زیر سناریو REGISTER که بالا اجرا کردیم را نمایش میده:

 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
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">

<scenario name="registration">

<send retrans="500">
<![CDATA[
REGISTER sip:[field1] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
Max-Forwards: 70
From: "sipp" <sip:[field0]@[field1]>;tag=[call_number]
To: "sipp" <sip:[field0]@[field1]>
Call-ID: reg///[call_id]
CSeq: 7 REGISTER
Contact: <sip:sipp@[local_ip]:[local_port]>
Expires: 3600
Content-Length: 0
User-Agent: SIPp
]]>
</send>

<recv response="100" optional="true">
</recv>

<recv response="401" auth="true" rtd="true">
</recv>

<send retrans="500">
<![CDATA[
REGISTER sip:[field1] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
Max-Forwards: 70
From: "sipp" <sip:[field0]@[field1]>;tag=[call_number]
To: "sipp" <sip:[field0]@[field1]>
Call-ID: reg///[call_id]
CSeq: 8 REGISTER
Contact: <sip:1100@[local_ip]:[local_port]>
Expires: 3600
Content-Length: 0
User-Agent: SIPp
[field2]
X-TVT: [field3]
]]>
</send>

<recv response="100" optional="true">
</recv>

<recv response="200">
</recv>

<ResponseTimeRepartition value="10, 20"/>
<CallLengthRepartition value="10"/>

</scenario>

قالب تعریف سناریوها XML هستش. ما با استفاده از تگ های <send> و <recv> از SIPp میخواهیم چه پیام های را به IPPBX ارسال کنه و منتظر دریافت چه دستوراتی باشه. هر پیام SIP هم باید با قالب مشخصی توی تگ ها تعریف بشن. این قالب در زیر نمایش داده شده:

1
<![CDATA[SIP-MESSAGE]]>  

به یک نکته باید دقت داشته باشیم. ابزار SIPp کنار فایل سناریو فایل تزریق یا Injection File هم قبول میکنه. این فایل در واقع یک CSV هستش که برای مقدار دهی به متغییرهای فایل سناریو استفاده میشه. ستون های این فایل از 0 شماره گذاری میشه و مقدار ستون nام با [fieldn] قابل دسترسی هستش. بعنوان مثال مقدار [feild3] را میتونیم بعنوان X-TVT در فایل سناریو استفاده کنیم.

پیروز باشید.