從0到1打造自己的網絡電話系統


最近流量卡越來越便宜了,看看自己手里的“坑不死老用戶”的聯通卡,頓時感覺到深深的惡意,但是iPhone沒有雙卡功能,所以只好自己動手打造一個網絡電話系統托管聯通卡,iPhone使用流量卡,系統轉移聯通卡的呼叫到iPhone上,其實也沒什么人給我打電話了[捂臉🤦‍♂️],主要是轉發短信,方便接受驗證碼。當然,反過來也可以,在iPhone上通過互聯網使用系統中的聯通卡撥號,和發短信,不過發短信基本上用不到了。

其實實現起來很簡單,就是使用是呀的各種現成的工具,累積木一樣搭出來。硬件清單如下:

  • Raspberry pi 3代B 一枚
  • 2A電源(我用的是iPad的充電器)
  • 16GB的SD卡+可讀寫SD卡的讀卡器
  • 華為的上網卡托E169

開始

已經有封裝了Asterisk和FreePBX的系統:RasPBX

  1. 首先下載RasPBX系統,燒錄到SD卡中,通電啟動樹莓派,接上顯示器和鍵盤(或者不用外接硬件),連上無線網。

    配置ssh登錄。

  2. 按照RasPBX的安裝教程首先應該raspbx-upgrade,但是在我大天朝就別想那么順利,需要那個啥你懂的,至於如何那個啥大家就各顯神通吧,如果你和我一樣使用ss那么可以參考這篇文章

    如果一切順利,那么proxychains raspbx-upgrade,大概等個也許10多分鍾就能更新完畢

  3. FreePBX提供了一個友好的Web界面供我們使用,在瀏覽器中輸入http://raspbx,mac輸入http://raspbx.local登錄,選擇Administrator,使用默認初始賬號admin,密碼admin進行登錄。

  4. 進入管理控制台后,首先映入眼簾的是儀表盤
    Screen Shot 2017-06-28 at 15.26.13.png

    先在局域網中測試一下能否正常使用,然后在到公網上去。由於是在局域網中測試,所以配置很簡單,直接在Applications->Extensions->Add new Chan_SIP Extension新建一個分機,在Generaltab頁里面填好第一個SIP賬戶的信息,例如
    Screen Shot 2017-07-07 at 11.48.43.png

    然后在Advanced中設置NAT ModeYes - (force_rport,comedia)

    最后點擊右下角的submit,

    再去Settings->Asterisk SIP Settings里面的Chan SIP Settingstab頁下面的第一個NAT設置未Never,然后Submit

    最后的最后點擊右上角的Apply Config應用剛才的配置。

  5. 接下來去下載一個SIP客戶端,我選擇的是zoiper,安裝完成之后,在Account中添加剛才在FreePBX中添加的賬戶,記住密碼是secret中的值,千萬別填下面的,那個是該用戶進入管理控制台的密碼,如果用戶名或者密碼不正確只會返回403而不會提示用戶名或者密碼錯誤。這里的Domain就填寫RasPBX在局域網中的ip地址。

    填好之后,點擊上方的Register

  6. 如果只有一個手機的話,那么可以在電腦上再裝一個SIP軟件,我在Mac上安裝了Telephone,然后重復上面的步驟再注冊另一個賬戶,在Mac上登錄。

    打一個電話測試一下吧,是不是很激動:)

內網測試通過之后,就可以開始搭建外網訪問了

  1. 現在可以嘗試通過外網訪問了,一般常見的有三種外網訪問情況。

能力 責任? 狗屁? 愛誰誰

1. 如果你有公網IP,那么這是最省事的,可惜大多數人沒有。

1. 用動態域名進行訪問

1. 沒有路由器權限或者路由器沒有被分配公網IP,這種情況只能內網穿透。

我沒有公網IP,所以1不行,本來我家里的路由器也沒有被分配公網IP,因為路由器連接電信的光貓使用DHCP聯網的,公網IP被分配到了電信的光貓上了,所以選擇3,進行內網穿透,使用ssh反向代理,但是發現ssh可以轉發TCP,對於UDP就無能為力了,於是打算把SIP的協議改成TCP,但是發現通信用的RTP只能使用UDP,沒法改。因此,又琢磨着使用IAX,結果發現IAX好像也改不成UDP,或許是我的姿勢不對。不得已只能試試2了,本來想破解電信的光貓,然后將其改成橋接模式,讓路由器進行PPOE撥號上網,看了很多破解教程,發現網上流傳的漏洞都被堵死了,至此打算放棄了,最后抱着試試看的心態,聯系了電信客服,客服說已為我報修,之后會有工程師聯系我,過了不久,工程師給我來電了,我說要把光貓改成橋接模式,工程師很爽快的答應了,立馬就遠程改好了,叫我過20分鍾重啟,重啟之后果然變成了橋接模式。接着我在路由器中使用PPPOE進行撥號上網,路由器就被分配了公網IP,真是踏破鐵鞋無覓處,得來全不費功夫:)
  1. 使用動態域名進行訪問

    先拿到路由器被分配的公網IP,可以直接在路由器中查看,或者在路由器下面的樹莓派中通過終端:curl ip.cn查看該IP。

    打開freepbx的web界面,登錄管理員界面,Settings->Asterisk SIP SettingsDetect Network Settings,自動檢測IP,應該和上一步中拿到的IP相同,再檢查下面的內網地址是否正確。
    Screen Shot 2017-07-07 at 11.53.37.png
    然后去Chan SIP Settingstab頁下面的NAT設置Static IP ,默認值就是之前檢測到的結果,如果沒錯就不用改,然后submit,applyconfig
    Screen Shot 2017-07-07 at 11.55.13.png

    再到路由器中設置端口轉發,因為那個公網IP是路由器的地址。每種路由器的設置都不同,請自行摸索一下,其實很簡單,就是把路由器的5060端口轉發到樹莓派的5060端口,協議最好選擇TCP/UDP,還有10001~20000端口也要轉發到樹莓派的10001~20000,這個是RTP通信端口可以選擇轉發UDP.這里沒必要轉發一萬個端口,一次通信只需要4個端口,所以轉4個端口10001~10004用來測試就可以了。

    現在去客戶端中把Domain改成公網IP,應該能夠撥通分機。

    用手機的4G網絡再試一次,還是正常那就通過了。

  2. 因為這個IP是電信運營商動態分配的,隨時都有可能變化,所以需要不斷檢測當前IP地址,然后通過DNS服務更改解析。最簡單的做法可以直接用花生殼等服務商的服務,不過我不喜歡這些服務商,打算自己寫個腳本來實現,這樣既有成就感又能完全掌握,用花生殼的客戶端給我一種感覺:總有刁民想害朕。

    雲計算井噴式的發展,我等小P民也能美美的用上了,去阿里雲買個便宜的ip地址和dns雲解析一年也才50多塊,阿里雲的雲解析的TTL最快達到1s。
    Screen Shot 2017-07-07 at 12.02.27.png

    樹莓派默認已經安裝了python環境,那就用python吧,調用阿里雲的sdk就可以了,分分鍾的事情。腳本參考連接,原作者已經寫得很好了,只是有一個異常情況作者沒有遇到,那就是ip.cn宕機了,所以我在腳本中簡單處理了一下這種情況。
    在樹莓派中,新建vim aliyun_ddns.py,把下面的代碼復制進去,部分地方根據實際情況修改。

    # -*- coding: UTF-8 -*-
    
    import json
    import os
    import re
    import sys
    from datetime import datetime
    
    from aliyunsdkalidns.request.v20150109 import UpdateDomainRecordRequest,    DescribeDomainRecordsRequest, \
        DescribeDomainRecordInfoRequest
    from aliyunsdkcore import client
    
    #請填寫你的Access Key ID
    access_key_id = "你的keyID"
    
    #請填寫你的Access Key Secret
    access_Key_secret = "你的Secret"
    
    #請填寫你的賬號ID
    account_id = "你的賬號ID"
    
    #如果選擇yes,則運行程序后僅現實域名信息,並不會更新記錄,用於獲取解析記錄ID。
    #如果選擇NO,則運行程序后不顯示域名信息,僅更新記錄
    i_dont_know_record_id = 'yes'
    
    #請填寫你的一級域名
    rc_domain = '域名'
    
    #請填寫你的解析記錄
    rc_rr = '你的解析記錄'
    
    #請填寫你的記錄類型,DDNS請填寫A,表示A記錄
    rc_type = 'A'
    
    #請填寫解析記錄ID
    rc_record_id = '解析記錄ID'
    
    #請填寫解析有效生存時間TTL,單位:秒
    rc_ttl = '1'
    
    #請填寫返還內容格式,json,xml
    rc_format = 'json'
    
    
    def my_ip():
        get_ip_method = os.popen('curl -s ip.cn')
        get_ip_responses = get_ip_method.readlines()[0]
        get_ip_pattern = re.compile(r'\d+\.\d+\.\d+\.\d+')
        get_ip_value = get_ip_pattern.findall(get_ip_responses)[0]
        return get_ip_value
    
    
    def check_records(dns_domain):
        clt = client.AcsClient(access_key_id, access_Key_secret, 'cn-hangzhou')
        request = DescribeDomainRecordsRequest.DescribeDomainRecordsRequest()
        request.set_DomainName(dns_domain)
        request.set_accept_format(rc_format)
        result = clt.do_action_with_exception(request)
        return result
    
    
    def old_ip():
        clt = client.AcsClient(access_key_id, access_Key_secret, 'cn-hangzhou')
        request = DescribeDomainRecordInfoRequest.DescribeDomainRecordInfoRequest()
        request.set_RecordId(rc_record_id)
        request.set_accept_format(rc_format)
        result = clt.do_action_with_exception(request)
        result = json.JSONDecoder().decode(result)
        result = result['Value']
        return result
    
    
    def update_dns(dns_rr, dns_type, dns_value, dns_record_id, dns_ttl, dns_format):
        clt = client.AcsClient(access_key_id, access_Key_secret, 'cn-hangzhou')
        request = UpdateDomainRecordRequest.UpdateDomainRecordRequest()
        request.set_RR(dns_rr)
        request.set_Type(dns_type)
        request.set_Value(dns_value)
        request.set_RecordId(dns_record_id)
        request.set_TTL(dns_ttl)
        request.set_accept_format(dns_format)
        result = clt.do_action_with_exception(request)
        return result
    
    def write_to_file():
        time_now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        current_script_path = sys.path[7]
        print current_script_path
        log_file = current_script_path + '/' + 'aliyun_ddns_log.txt'
        write = open(log_file, 'a')
        write.write(time_now + ' ' + str(rc_value) + '\n')
        write.close()
        return 
    
    if i_dont_know_record_id == 'yes':
        print check_records(rc_domain)
    elif i_dont_know_record_id == 'no':
        try:
          rc_value = my_ip()
        except:
          rc_value = old_ip()
        rc_value_old = old_ip()
        if rc_value_old == rc_value:
            print 'The specified value of parameter Value is the same as old'
        else:
            print update_dns(rc_rr, rc_type, rc_value, rc_record_id, rc_ttl, rc_format)
            write_to_file()
    

    然后導入阿里雲的python sdk:pip install aliyun-python-sdk-alidns

    先去阿里雲控制台手動解析一下,生成一個記錄就OK了。

    首次運行腳本前,把上面的腳本中的i_dont_know_record_id填寫yes,然后運行python /path/of/aliyun_ddns.py,拿到結果中就包含了你的解析記錄ID,

    {
    "PageNumber": 1,
    "TotalCount": 1,
    "PageSize": 1,
    "RequestId": "xxxxx-xxxx-xxxx-xxxx-xxxxxx",
    "DomainRecords": {
        "Record": [
        {
            "RR": "xxxx",
            "Status": "xxxxx",
            "Value": "xxxxxx",
            "RecordId": "xxxxxxx",
            "Type": "A",
            "DomainName": "xxxx",
            "Locked": false,
            "Line": "default",
            "TTL": "1"
        }
        ]
    }
    }
    

    RecordId的值填到腳本中rc_record_id,然后把i_dont_know_record_id改為no
    再次執行,如果沒有異常那就通過了。

    然后用cron做成定時任務。
    輸入crontab -e,添加:

    */1 * * * * /usr/bin/python2.7 ~/aliyun_ddns.py > /dev/null 1>/dev/null
    

    注意腳本的路徑。

  3. 去FreePBX的管理控制台中,Settings->Asterisk SIP SettingsDetect Network Settings,將其刪除,這個輸入框留空,因為我們要用動態域名,所以這里不能填IP,到Chan SIP Settingstab頁下面的NAT設置Dynamic IP ,輸入買來的域名,下面的檢測間隔設置為60s。Submit->Apply Config

  4. 
    注意:最好在路由器中設置給樹莓派一個靜態ip,防止樹莓派的ip地址變化了,導致路由器的端口轉發實效。

  5. 在用客戶端中,把Domain改成域名,測試一遍,分機之間可以相互通信。

使用上網卡托接打電話

  1. 這時候就需要3G chan_gongle了,我是在某寶上買的華為E169。chan_dongle兼容的產品列表

    先安裝驅動install-dongle,安裝過程中會詢問幾個問題,認真作答:)

    Please enter the phone number of your SIM card (defaults to +1234567890 if left blank):
    輸入U棒里的 sim卡手機號碼,直接回車則默認為 +1234567890
    
    Send incoming SMS to email address (leave empty to disable SMS forwarding):
    設置郵箱,以便將U棒收到的短信內容轉發過去,直接回車可取消該功能
    
    Forward incoming SMS to mobile phone number (via dongle0) (leave empty to disable):
    設置一個手機號碼,以便將U棒收到的短信通過 dongle0 轉發至該號碼,直接回車可取消該功能
    
    Would you like to install a webpage for sending SMS with chan_dongle? (http://raspbx/sms/) [y/N]
    是否安裝發送短信的web頁面,可回答 y 並按提示設置一個登錄密碼。
    
  2. 增加一個trunk:connectivity->Trunks
    Screen Shot 2017-07-07 at 12.54.02.png
    填上dongle/dongle0/$OUTNUM$
    Screen Shot 2017-07-07 at 12.54.45.png

    Submit->ApplyConfig

  3. 編寫撥號路由:connectivity->Outbound Routes,命名Route Name,選擇剛才新建的Trunk
    Screen Shot 2017-07-07 at 12.56.53.png

    設置撥號規則,匹配號碼,如果你的號碼匹配這個規則,那么就使用咱們的Trunk撥號,這里我添加了兩個規則:1.匹配1開頭的所有號碼,prefix=9即第一數字是9的所有號碼。
    Screen Shot 2017-07-07 at 13.00.03.png

    Submit->ApplyConfig

  4. 編寫接聽路由:connectivity->Inbound Routes,填寫Description描述一下,Set Destination目標選擇801分機
    Screen Shot 2017-07-07 at 13.03.45.png
    Submit->ApplyConfig

  5. 現在在客戶端中撥號1**********,應該就可以撥通了,再發條短信到我的號碼上,應該也會自動轉發到目標號碼上。不過短信轉發支持不完善,經常缺失內容,所以干脆禁用該功能,使用郵件轉發收到的短信。

通過郵件轉發收到的短信

  1. 配置exim4:dpkg-reconfigure exim4-config或者直接修改文件/etc/exim4/update-exim4.conf.conf,例如我使用自己的QQ郵箱,其他郵箱自行在郵箱設置中找到對應的配置

    dc_eximconfig_configtype='smarthost'
    dc_other_hostnames='raspberrypi'
    dc_local_interfaces='127.0.0.1'
    dc_readhost=''
    dc_relay_domains=''
    dc_minimaldns='false'
    dc_relay_nets=''
    dc_smarthost='smtp.qq.com'
    CFILEMODE='644'
    dc_use_split_config='false'
    dc_hide_mailname='false'
    dc_mailname_in_oh='true'
    dc_localdelivery='mail_spool'
    
  2. smtp的帳號密碼設置/etc/exim4/passwd.client

    smtp.qq.com:郵箱賬號:郵箱密碼
    

    我這里的郵箱密碼使用的是騰訊的授權碼。

  3. 系統郵箱地址/etc/email-addresses

    root: 郵箱賬號
    asterisk: 郵箱賬號
    

    這里一定要填寫asterisk,因為chan_dongle使用帳戶asterisk調用sendemil命令,所以如果不寫,郵箱服務端不認可。添加root,是為了測試用。

    update-exim4.conf更新一下。

    命令send_test_email your_email@someisp.com,測試一下是否能夠發送成功。

  4. 注意日志文件是/var/log/exim4/mainlog,如果有任何問題先去日志里看看。

  5. 配置/etc/asterisk/dongle.conf

    context=from-trunk              ; context for incoming calls
    group=0                         ; calling group
    rxgain=0                        ; increase the incoming volume; may be negative
    txgain=0                        ; increase the outgoint volume; may be negative
    autodeletesms=yes               ; auto delete incoming sms
    resetdongle=yes                 ; reset dongle during initialization with ATZ command
    u2diag=0                        ; set ^U2DIAG parameter on device (0 = disable  everything except modem function) ; -1 not use ^U2DIAG command
    

    繼續往下翻,配置dongle0,ttyUSB*和你系統中的保持一致,底下的imeiimsi可以通過命令:asterisk -rx "dongle show devices"找到。

    ; dongle required settings
    [dongle0]
    audio=/dev/ttyUSB1              ; tty port for audio connection;        no default  value
    data=/dev/ttyUSB2               ; tty port for AT commands;             no default  value
    
    ; or you can omit both audio and data together and use imei=123456789012345 and/or  imsi=123456789012345
    ;  imei and imsi must contain exactly 15 digits !
    ;  imei/imsi discovery is available on Linux only
    imei=xxxx
    imsi=xxxx
    
  6. 設置chan_dongle的/etc/asterisk/extensions_custom.conf

    [from-trunk]
    exten => s,n,goto(from-trunk,${DONGLEIMEI},1)
    
    exten => sms,1,Verbose(Incoming SMS from ${CALLERID(num)} ${SMS})
    exten => sms,n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${DONGLENAME}    - ${CALLERID(num)}: ${SMS}' >> /var/log/asterisk/sms.txt)
    exten => sms,n,System( echo "Receiver:${DONGLENUMBER}\nDate:${STRFTIME(${EPOCH},,   %Y-%m-%d %H:%M:%S)}\nContent:${SMS}" | /usr/bin/mail -s '[SMS]From:${CALLERID(num)}'   郵箱地址)
    exten => sms,n,Hangup()
    
    exten => ussd,1,Verbose(Incoming USSD: ${USSD})
    exten => ussd,n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${DONGLENAME}   : ${USSD}' >> /var/log/asterisk/ussd.txt)
    exten => ussd,n,Hangup()
    
  7. 輸入命令amportal admin reload,更新配置。

  8. 輸入asterisk -vvvvvr進入調試模式,觀察調試信息,這時候發送一條短信到你的號碼上,

    Incoming SMS from 對方號碼 test
    -- Executing [sms@from-trunk:2] System("Local/sms@from-trunk-00000011;1", "echo '2017-07-06 18:08:18 - dongle0 - 對方號碼: test' >> /var/log/asterisk/sms.txt") in new stack
    -- Executing [sms@from-trunk:3] System("Local/sms@from-trunk-00000011;1", " echo "Receiver:+我的號碼\nDate:2017-07-06 18:08:18\nContent:test" | /usr/bin/mail -s '[SMS]From:對方號碼' 我的郵箱") in new stack
    -- Executing [sms@from-trunk:4] Hangup("Local/sms@from-trunk-00000011;1", "") in new stack
    == Spawn extension (from-trunk, sms, 4) exited non-zero on    'Local/sms@from-trunk-00000011;1'
      -- Executing [h@from-trunk:1] Macro("Local/sms@from-trunk-00000011;1", "hangupcall,") in new stack
      -- Executing [s@macro-hangupcall:1] GotoIf("Local/sms@from-trunk-00000011;1", "1?theend") in new stack
      -- Goto (macro-hangupcall,s,3)
      -- Executing [s@macro-hangupcall:3] ExecIf("Local/sms@from-trunk-00000011;1", "0?Set(CDR(recordingfile)=)") in new stack
      -- Executing [s@macro-hangupcall:4] Hangup("Local/sms@from-trunk-00000011;1", "") in new stack
    == Spawn extension (macro-hangupcall, s, 4) exited non-zero on    'Local/sms@from-trunk-00000011;1' in macro 'hangupcall'
    == Spawn extension (from-trunk, h, 1) exited non-zero on  'Local/sms@from-trunk-00000011;1'
    
  9. 如果郵件發送失敗,注意觀察日志tail /var/log/exim4/mainlog


尾聲

  1. 使用TCP優化通信

    其實很簡單,Settings->Asterisk SIP Settings里面的Chan SIP Settings這個tab頁下面的Enable TCP選擇Yes,然后Submit,別忘了Apply Config.

    然后再把之前建好的分機Extensions改一下協議。Applications->ExtensionsAdvanced中設置TransportTCP Only,這里先改一個zoiper客戶端使用的分機Extension

    至此可以用TCP協議了,那么在客戶端中賬戶的Network Settings里面的Transport設置為TCP,而Telephone只能用UDP,因此上一步中不要改Telephone上用的分機Extension

    OK,再用手機上的Zoiper打電話給Telephone,如果通了那就OK。

  2. 錦上添花

    為了在DNS解析不及時等異常情況下,能夠遠程登錄到樹莓派上進行操作,可以用ssh進行反向代理控制樹莓派,其實動手比較簡單,但是原理比較難懂。使用ssh開啟反向代理隧道,讓訪問服務器的某個端口都被ssh轉發到本地的5060端口。

    這一步比較難懂,但是不難實現,參考下面的教程:

    1. 先用ssh搭建反向隧道,再用autossh保證隧道的穩定

    2. 然后做成服務使其能夠開機自動啟動,注意最好指定autossh的監控端口

    這里面要注意服務器的防火牆要對以上所需的TCP端口打開。

  3. 通過ssh反向代理暴露FreePBX的WebUI

    官方提供了3種安全的暴露FreePBX的WebUI的姿勢:

    1. 使用SSL
    2. 使用VPN
    3. 使用SSH反向代理
      千萬不要直接暴露在公網上,即便管理員密碼設的很復雜,因為官方說攻擊者可以利用PHP的漏洞,切記!!!
      有了上一步的經驗,這一步就很簡單了
      ssh -fNR *:8800:localhost:80 服務器賬號@服務器地址,這樣直接訪問服務器的8800端口就能進入WebUI管理界面了。
  4. 使用Fail2Ban保證安全

    我的FreePBX剛暴露到公網就被人不斷的強行破解,真是惡心的不行。

    參考這篇文章學習下怎么使用Fail2Ban

    然后參考這個鏈接配置asterisk

The Last Thing

千萬別忘記備份一下單月沒有問題好了吧zhe這把應該不會有問題了吧,簡直了

再見也許不再見

離別或許成永別

仗義執言勇氣可嘉


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM