Tag Archives: FreeSWITCh

FreeSWITCH Bridge Timers 101

A cheat sheet for anyone trying to control FreeSWITCH bridge behaviour if you’re trying to move calls around if not answered / responded to:

originate_timeout

How long to wait for any response to from the remote peer (100 TRYING, 180 RINGING, etc).

This is useful for knowing when to give up and try a different peer as this peer is dead.

originate_retries

How many times to retransmit the INVITE if no 100 TRYING / 180 RINGING is received.

Like originate_timeout, this is handy for giving up sooner when a peer is dead and moving onto others.

progress_timeout

How long we wait between sending the SIP INVITE before we get a 180/183 before we give up.

This is handy to find out if the remote end isn’t able to reach the endpoint you’re after (page the UE in a cellular context).

bridge_answer_timeout

How long do we wait between the INVITE and a 200 OK (Including RINGING) – This is useful for “no answer” timeouts.

If you want to know why a bridge failed, ie no answer timeout reached, error on the remote end, etc, we can see why with the following variable:

variable_last_bridge_hangup_cause: [PROGRESS_TIMEOUT]

Which will allow you to tell if it’s no answer or progress timeout to blame.

FreeSWITCH – Keep Call-ID the same on both legs of a Bridged Call

I needed to have both legs of the B2BUA bridge call through FreeSWITCH using the same Call-ID (long story), and went down the rabbit hole of looking for how to do this.

A post from 15 years ago on the mailing list from Anthony Minessale said he added “sip_outgoing_call_id” variable for this, and I found the commit, but it doesn’t work – More digging shows this variable disappears somewhere in history.

But by looking at what it changed I found sip_invite_call_id does the same thing now, so if you want to make both legs use the same Call-ID here ya go:

<action application="set" data="sip_invite_call_id=mycustomcallid"/>

Yeah, this post probably could’ve been a Tweet….

Using fs_cli and ESL

If you work with FreeSWITCH there’s a good chance every time you do, you run fs_cli and attempt to read the firehose of data shown when making a call to make sense of what’s going on and why what you’re trying to do isn’t working.

But, if you are also using the Event Socket Language service built into FreeSWITCH (Which you totally should) either for programming FreeSWITCH behaviour, or for realtime charging with CGrateS, then you’ll find that fs_cli will fail to connect.

That’s because we’ve edited the event_socket.conf.xml file, and fs_cli uses the event socket to connect to FreeSWITCH as well.

But there’s a simple fix,

Create a new file in /etc/fs_cli.conf and populate it with the info needed to connect to your ESL session you defined in event_socket.conf.xml, so if this is is your

<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <param name="nat-map" value="false"/>
    <param name="listen-ip" value="10.98.0.76"/>
    <param name="listen-port" value="8021"/>
    <param name="password" value="mysupersecretpassword"/>
    <param name="apply-inbound-acl" value="NickACL"/>
  </settings>
</configuration>

Then your /etc/fs_cli.conf should look like:

[default]
; Put me in /etc/fs_cli.conf or ~/.fs_cli_conf
;overide any default options here
loglevel => 6
log-uuid => false


host     => 10.98.0.76
port     => 8021
password => mysupersecretpassword
debug    => 7

And that’s it, now you can run fs_cli and connect to the terminal once more!

HOMER API in Python

We’re doing more and more network automation, and something that came up as valuable to us would be to have all the IPs in HOMER SIP Capture come up as the hostnames of the VM running the service.

Luckily for us HOMER has an API for this ready to roll, and best of all, it’s Swagger based and easily documented (awesome!).

(Probably through my own failure to properly RTFM) I was struggling to work out the correct (current) way to Authenticate against the API service using a username and password.

Because the HOMER team are awesome however, the web UI for HOMER, is just an API client.

This means to look at how to log into the API, I just needed to fire up Wireshark, log into the Web UI via my browser and then flick through the packets for a real world example of how to do this.

Homer Login JSON body as seen by Wireshark

In the Login action I could see the browser posts a JSON body with the username and password to /api/v3/auth

{"username":"admin","password":"sipcapture","type":"internal"}

And in return the Homer API Server responds with a 201 Created an a auth token back:

Now in order to use the API we just need to include that token in our Authorization: header then we can hit all the API endpoints we want!

For me, the goal we were setting out to achieve was to setup the aliases from our automatically populated list of hosts. So using the info above I setup a simple Python script with Requests to achieve this:

import requests
s = requests.Session()

#Login and get Token
url = 'http://homer:9080/api/v3/auth'
json_data = {"username":"admin","password":"sipcapture"}
x = s.post(url, json = json_data)
print(x.content)
token = x.json()['token']
print("Token is: " + str(token))


#Add new Alias
alias_json = {
          "alias": "Blog Example",
          "captureID": "0",
          "id": 0,
          "ip": "1.2.3.4",
          "mask": 32,
          "port": 5060,
          "status": True
        }

x = s.post('http://homer:9080/api/v3/alias', json = alias_json, headers={'Authorization': 'Bearer ' + token})
print(x.status_code)
print(x.content)


#Print all Aliases
x = s.get('http://homer:9080/api/v3/alias', headers={'Authorization': 'Bearer ' + token})
print(x.json())

And bingo we’re done, a new alias defined.

We wrapped this up in a for loop for each of the hosts / subnets we use and hooked it into our build system and away we go!

With the Homer API the world is your oyster in terms of functionality, all the features of the Web UI are exposed on the API as the Web UI just uses the API (something I wish was more common!).

Using the Swagger based API docs you can see examples of how to achieve everything you need to, and if you ever get stuck, just fire up Wireshark and do it in the Homer WebUI for an example of how the bodies should look.

Thanks to the Homer team at QXIP for making such a great product!

Sangoma Transcoding Cards Setup

The Wiki on the Sangoma documentation page is really out of date and can’t be easily edited by the public, so here’s the skinny on how to setup a Sangoma transcoding card on a modern Debian system:

apt-get install libxml2* wget make gcc
wget https://ftp.sangoma.com/linux/transcoding/sng-tc-linux-1.3.11.x86_64.tgz
tar xzf sng-tc-linux-1.3.11.x86_64.tgz
cd sng-tc-linux-1.3.11.x86_64/
make
make install
cp lib/* /usr/local/lib/
ldconfig

At this point you should be able to check for the presence of the card with:

sngtc_tool -dev ens33 -list_modules

Where ens33 is the name of the NIC that the server that shares a broadcast domain with the transcoder.

Successfully discovering the Sangoma D150 transcoder

If instead you see something like this:

root@fs-131:/etc/sngtc#  sngtc_tool -dev ens33 -list_modules
Failed to detect and initialize modules with size 1

That means the server can’t find the transcoding device. If you’re using a D150 (The Ethernet enabled versions) then you’ve got to make sure that the NIC you specified is on the same VLAN / broadcast domain as the server, for testing you can try directly connecting it to the NIC.

I also found I had to restart the device a few times to get it to a “happy” state.

It’s worth pointing out that there are no LEDs lit when the system is powered on, only when you connect a NIC.

Next we’ll need to setup the sngtc_server so these resources can be accessed via FreeSWITCH or Asterisk.

Config is pretty simple if you’re using an all-in-one deployment, all you’ll need to change is the NIC in a file you create in /etc/sngtc/sngtc_server.conf.xml:

<configuration name="sngtc_server.conf" description="Sangoma Transcoding Manager Configuration">

        <settings>
                <!--
                By default the SOAP server uses a private local IP and port that will work for out of the box installations
                where the SOAP client (Asterisk/FreeSWITCH) and server (sngtc_server) run in the same box.
                However, if you want to distribute multiple clients across the network, you need to edit this values to
                listen on an IP and port that is reachable for those clients.
                <param name="bindaddr" value="0.0.0.0" />
                <param name="bindport" value="9000" />
                -->
        </settings>

        <vocallos>

                <!-- The name of the vocallo is the ethernet device name as displayed by ifconfig -->
                <vocallo name="ens33">
                        <!-- Starting UDP port for the vocallo -->
                        <param name="base_udp" value="5000"/>
                        <!-- Starting IP address octet to use for the vocallo modules -->
                        <param name="base_ip_octet" value="182"/>
                </vocallo>

        </vocallos>


</configuration>

With that set we can actually try starting the server,

Again, all going well you should see something like this in the log:

And then at the end you should see:

[SNGTC_INFO ] * 16:43:58: [00-0c-xx-yy-zz] RoundTripMs = 6 ulExtractTimeMs=0 ulCmdTimeoutMs 1000
[SNGTC_INFO ] * 16:43:58: 00-0c-xx-yy-zz: Reset Finished

[SNGTC_INFO ] * 16:43:58: 00-0c-xx-yy-zz: Setting cpu threshold Hi=90/Lo=80
[SNGTC_INFO ] * 16:43:58: Sangoma Transcoding Server Ready
[SNGTC_INFO ] * 16:43:58: Monitoring Sangoma Transcoding Modules

Once we know it’s starting up manually we can try and start the daemon.

sngtc_server_ctrl start

Should result in:

sngtc_server: Starting sngtc_server in safe mode ...
sngtc_server: Starting processes...
Starting sngtc_server...OK

And with that, we’re off and running and ready to configure this for use in FreeSWITCH or Asterisk.

Get all the FreeSWITCH Folder Paths

Thanks to it’s reliability, I find I go long periods of time without needing to do anything on FreeSWITCH.

But every now and then I log into a system and I can’t find the path I’m looking for, where do the recordings get stored?

The CDR storage location?

Here’s a simple trick to show the directory paths for a FreeSWITCH instance:

fs_cli -x 'global_getvar'| grep _dir

This will output all the paths you could possibly want:

nick@fs-131:~$ fs_cli -x 'global_getvar'| grep _dir
base_dir=/usr
recordings_dir=/var/lib/freeswitch/recordings
sounds_dir=/usr/share/freeswitch/sounds
conf_dir=/etc/freeswitch
log_dir=/var/log/freeswitch
run_dir=/var/run/freeswitch
db_dir=/var/lib/freeswitch/db
mod_dir=/usr/lib/freeswitch/mod
htdocs_dir=/usr/share/freeswitch/htdocs
script_dir=/usr/share/freeswitch/scripts
temp_dir=/tmp
grammar_dir=/usr/share/freeswitch/grammar
fonts_dir=/usr/share/freeswitch/fonts
images_dir=/var/lib/freeswitch/images
certs_dir=/etc/freeswitch/tls
storage_dir=/var/lib/freeswitch/storage
cache_dir=/var/cache/freeswitch
data_dir=/usr/share/freeswitch
localstate_dir=/var/lib/freeswitch

Saved me a lot of poking around, hopefully it’ll make others lives easier too.

CGrateS in Baby Steps – Part 4 – Rating Calls

In our last few posts we got CGrateS setup in order to have rates and tariffs in the system, so we can price a call.

Where we ended we were able to use the APIerSv1.GetCost method to get the cost of a call, and today, we’re going to actually create some rated CDRs.

So again this will be done through the API, using the CDRsV1.ProcessExternalCDR method.

So let’s give it a whirl:

#Add a CDR
print("Testing call..")
cdr = CGRateS_Obj.SendData({"method": "CDRsV1.ProcessExternalCDR", "params": [ { \
"Direction": "*out",
    "Category": "call",
    "RequestType": "*raw",
    "ToR": "*monetary",
    "Tenant": "cgrates.org",
    "Account": "1002",
    "Subject": "1002",
    "Destination": "61411111",
    "AnswerTime": "2022-02-15 13:07:39",
    "SetupTime": "2022-02-15 13:07:30",
    "Usage": "181s",
    "OriginID": "API Function Example"
    }], "id": 0})
pprint.pprint(cdr)

So the output of this, you may notice returns “Partially Executed” in the output, that’s no good.

{'method': 'CDRsV1.ProcessExternalCDR', 'params': [{'Direction': '*out', 'Category': 'call', 'RequestType': '*raw', 'ToR': '*monetary', 'Tenant': 'cgrates.org', 'Account': '1002', 'Subject': '1002', 'Destination': '61411111', 'AnswerTime': '2022-02-15 13:07:39', 'SetupTime': '2022-02-15 13:07:30', 'Usage': '181s', 'OriginID': 'API Function Example'}], 'id': 0}
OrderedDict([('id', 0), ('result', None), ('error', 'PARTIALLY_EXECUTED')])

So what’s going on here?

Well, there’s another concept I haven’t introduced yet, and that’s ChargerS, this is a concept / component we’ll dig into deeper for derived charging, but for now just know we need to add a ChargerS rule in order to get CDRs rated:

#Define Charger
print(CGRateS_Obj.SendData({
    "method": "APIerSv1.SetChargerProfile",
    "params": [
        {
            "Tenant": "cgrates.org",
            "ID": "DEFAULT",
            'FilterIDs': [],
            'AttributeIDs' : ['*none'],
            'Weight': 0,
        }
    ]   }   ))   
#Set Charger
print("GetChargerProfile: ")
GetChargerProfile = CGRateS_Obj.SendData({"jsonrpc": "2.0", "method": "ApierV1.GetChargerProfile", "params": [{"TPid": "cgrates.org", "ID" : "DEFAULT"}]})
print("GetChargerProfile: ")
pprint.pprint(GetChargerProfile)

Now if we try rating the CDR again we should get a successful output:

{'method': 'CDRsV1.ProcessExternalCDR', 'params': [{'Direction': '*out', 'Category': 'call', 'RequestType': '*raw', 'ToR': '*monetary', 'Tenant': 'cgrates.org', 'Account': '1002', 'Subject': '1002', 'Destination': '6141111124211', 'AnswerTime': '2022-02-15 13:07:39', 'SetupTime': '2022-02-15 13:07:30', 'Usage': '181s', 'OriginID': 'API Function Example'}], 'id': 0}
OrderedDict([('id', 0), ('result', 'OK'), ('error', None)])

Great, so where did the CDR go?

Well, if you’ve got CDR storage in StoreDB enabled (And you probably do if you’ve been following up until this point), then the answer is a MySQL table, and we can retrive the data with:

sudo mysql cgrates -e "select * from cdrs \G"

For those of you with a bit of MySQL experience under your belt, you’d be able to envisage using the SUM function to total a monthly bill for a customer from this.

Of course we can add CDRs via the API, and you probably already guessed this, but we can retrive CDRs via the API as well, filtering on the key criteria:

#Get CDRs
cdrs = CGRateS_Obj.SendData({"method": "ApierV1.GetCDRs", "params": [ { \
"Direction": "*out",
   "Tenants": ["cgrates.org"],
   "Accounts": ["1002"],
    "TimeStart": "2022-02-14 13:07:39",
    "TimeEnd": "2022-02-16 13:07:39",
    "Limit": 100
    }], "id": 0})
pprint.pprint(cdrs)

This would be useful for generating an invoice or populating recent calls for a customer portal.

Maybe creating rated CDRs and sticking them into a database is exactly what you’re looking to achieve in CGrateS – And if so, great, this is where you can stop – but for many use cases, there’s a want for an automated solution – For your platform to automatically integrate with CGrateS.

If you’ve got an Asterisk/FreeSWITCH/Kamailio or OpenSIPs based platform, then you can integrate CGrateS directly into your platform to add the CDRs automatically, as well as access features like prepaid credit control, concurrent call limits, etc, etc.
The process is a little different on each of these platforms, but ultimately under the hood, all of these platforms have some middleware that generates the same API calls we just ran to create the CDR.

So far this tutorial has been heavy on teaching the API, because that’s what CGrateS ultimately is – An API service.

Our platforms like Asterisk and Kamailio with the CGrateS plugins are just CGrateS API clients, and so once we understand how to use and interact with the API it’s a breeze to plug in the module for your platform to generate the API calls to CGrateS required to integrate.

You can find all the code used in today’s lesson in the GitHub repo for this tutorial series.

CGrates – FreeSWITCH Interaction

In our last post we talked about setting rates in CGrates and testing them out, but what’s the point in learning a charging system without services to charge?

This post focuses on intergrating FreeSWITCH and CGrates, other posts cover integrating Asterisk and CGrates, Kamailio and CGrates and Diameter and CGrates.

Future posts in this series will focus on the CGrates side, but this post will be a bit of a sidebar to get our FreeSWITCH environment connected to CGrates so we can put all our rating and charging logic into FreeSWITCH.

CGrates interacts with FreeSWITCH via the Event-Socket-Language in FreeSWITCH, which I’ve written about before, in essence when enabled, CGrates is able to make decisions regarding if a call should proceed or not, monitor currently up calls, and terminate calls when a subscriber has used their allocated balance.

Adding ESL Binding Support in FreeSWITCH

The configuration for CGrates is defined through the cgrates.json file in /etc/cgrates on your rating server.

By default, FreeSWITCH’s event socket only listens on localhost, as it is a pretty huge security flaw to open it to the world, but in order for our CGrates server to be able to access we’ll need to bind it to an IP Address assigned to the FreeSWITCH server so we can reach it from elsewhere on the network.

<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <param name="nat-map" value="false"/>
    <param name="listen-ip" value="0.0.0.0"/>
    <param name="listen-port" value="8021"/>
    <param name="password" value="ClueCon"/>
    <param name="apply-inbound-acl" value="any_v4.auto"/>
  </settings>
</configuration>

Please setup the ACLs & password securely!

You may want to have CGrates installed on a different machine to your FreeSWITCH instance, or you may want to have multiple FreeSWITCH instances all getting credit control from CGrates.

Well, inside the cgrates.json config file, is where we populate the ESL connection details so CGrates can connect to FreeSWITCH.

"freeswitch_agent": {
        "enabled": true,
        "event_socket_conns":[
                {"address": "10.0.1.56:8021", "password": "ClueCon", "reconnects": -1,"alias":"Remote_FS_1"}
        ],
        "sessions_conns": ["*birpc_internal"],
        "empty_balance_ann_file": "/usr/share/freeswitch/sounds/en/us/callie/misc/8000/misc-your_call_has_been_terminated.wav",
        "empty_balance_ann_file": "/usr/share/freeswitch/sounds/en/us/callie/misc/8000/phone_not_auth.wav",
        "create_cdr": true
},

Dialplan Support

We’ll need to add the following config to our dialplan in order to tag in CGRates for the call.

 <extension name="unloop">
      <condition field="${unroll_loops}" expression="^true$" />
      <condition field="${sip_looped_call}" expression="^true$">
        <action application="deflect" data="${destination_number}" />
      </condition>
    </extension>
    <extension name="call_debug" continue="true">
      <condition field="${call_debug}" expression="^true$" break="never">
        <action application="info" />
      </condition>
    </extension>
   <extension name="CGRateS_Auth">
    <condition field="${cgr_notify}" expression="^$">
        <aciton application="log" data="In the CGRateS_Auth block" />
        <action application="info"/>
        <action application="park" />
      </condition>
    </extension>
    <extension name="CGRateS_AuthForbidden">
      <condition field="${cgr_notify}" expression="^-INSUFFICIENT_FUNDS$">
        <action application="log" data="Insufficent Funds" />
        <action application="set" data="proto_specific_hangup_cause=sip:403" />
        <action application="hangup" />
      </condition>
    </extension>
    <extension name="CGRateS_AuthForbidden">
      <condition field="${cgr_notify}" expression="^-UNAUTHORIZED_DESTINATION$">
        <action application="log" expression"CGrates Auth Forbidden" />
        <action application="set" data="proto_specific_hangup_cause=sip:403" />
        <action application="hangup" />
      </condition>
    </extension>
    <extension name="CGRateS_Error">
      <condition field="${cgr_notify}" expression="^-SYSTEM_ERROR$">
        <action application="set" data="proto_specific_hangup_cause=sip:503" />
        <action application="hangup" />
      </condition>
    </extension>
     <extension name="CGR Routes">
     <condition field="cgr_routes" expression=".+">
        <action application="log" data="In the CGR Routes block..." />
        <action application="set" data="cgr_route=${cgr_routes[1]}" />
      </condition>
    </extension>

Extension Support

Next we’ll need to tag the extensions we want to charge,

In order to do this we’ll need to set the type of the account (Ie. Prepaid, Postpaid, etc), and the flags to apply, which dictate which of the modules we’re going to use inside CGrateS.

FreeSWITCH won’t actually parse this info, it’s just passed to CGrateS.

<include>
  <user id="1001">
    <params>
      <param name="password" value="$${default_password}"/>
    </params>
    <variables>
      <variable name="accountcode" value="1001"/>
      <variable name="user_context" value="default"/>
      <variable name="effective_caller_id_number" value="1001"/>
      <variable name="outbound_caller_id_name" value="$${outbound_caller_name}"/>
      <variable name="outbound_caller_id_number" value="$${outbound_caller_id}"/>
      <variable name="cgr_reqtype" value="*prepaid"/>
      <variable name="cgr_flags" value="*resources;*attributes;*sessions;*routes;*thresholds;*stats;*accounts"/>
      <variable name="cgr_acd" value="30"/>
    </variables>
  </user>
</include>

If this is not set, the user won’t be charged.

And that’s pretty much it, when you restart FreeSWITCH and CGrates you should see in the CGrates log that it is connected to your FreeSWITCH instance, and when you make a call, FreeSWITCH will authorize it through CGrates.

We’ll get back into the nitty gritty about setting up CGrates in a future post, and cover setting up integration like this with other Platforms (Kamailio / Asterisk) and Protocols (Diameter & Radius) in future posts.

FreeSWITCH mod_python3 – Python Dialplans

Sometimes FreeSWITCH XML dialplan is a bit cumbersome to do more complex stuff, particularly to do with interacting with APIs, etc. But we have the option of using scripts written in Python3 to achieve our outcomes and pass variables to/from the dialplan and perform actions as if we were in the dialplan.

This is different to the Event Socket interface for Python I’ve covered in the past.

For starters we’ll need to install the module and enable it, here’s the StackOverflow thread that got me looking at it where I share the setup steps.

Here is a very simple example I’ve put together to show how we interact with Python3 in FreeSWITCH:

We’ll create a script in /usr/share/freeswitch/scripts/ and call it “CallerName.py”

from freeswitch import *
import sys 
def handler(session,args):

    #Get Variables from FreeSWITCH
    user_name = str(session.getVariable("user_name"))
    session.execute("log", "Call from Username: " + str(user_name))

    #Check if Username is equal to Nick
    if user_name == "Nick":
        session.execute("log", "Username is Nick!")
        #Convert the Username to Uppercase
        session.execute("set", "user_name=" + str(user_name).upper())
        #And return to the dialplan
        return
    else:
        #If no matches then log the error
        session.execute("log", "CRIT Username is not Nick - Hanging up the call")
        #And reject the call
        session.execute("hangup", "CALL_REJECTED")

Once we’ve created and saved the file, we’ll need to ensure it is owned by and executable by service user:

chown freeswitch:freeswitch CallerName.py
chmod 777 CallerName.py

In our Dialplan we’ll need to add the below to our logic to get called at the time we want it:

<action application="system" data="export PYTHONPATH=$PYTHONPATH:/usr/share/freeswitch/scripts/"/> <action application="python" data="CallerName"/>

After adding this to the dialplan, we’ll need to run a “reloadxml” to reload the dialplan, and now when these actions are hit, the Python script we created will be called, and if the user_name variable is set to “nick” it will be changed to “NICK”, and if it it isn’t, the call will be hung up with a “CALL_REJECTED” response.

Obviously this is a very basic scenario, but I’m using it for things like ACLs from an API, and dynamic call routing, using the familiar and easy to work with Python interpreter.

FreeSWITCH – Incompatible Destination

A recent little issue I ran into the other day, that I figured may be of use to someone in the future.

When making a call to FreeSWITCH I would get an “INCOMPATIBLE DESTINATION” response to the SIP INVITE.

Here’s what I saw in the log:

FreeSWITCH showing an “INCOMPATIBLE DESTINATION” error
2022-02-19 13:04:04.027963 99.47% [DEBUG] switch_core_media.c:5650 Audio Codec Compare [GSM:3:8000:20:13200:1]/[opus:116:48000:20:0:1]
2022-02-19 13:04:04.027963 99.47% [DEBUG] switch_core_media.c:5650 Audio Codec Compare [GSM:3:8000:20:13200:1]/[G722:9:8000:20:64000:1]
2022-02-19 13:04:04.027963 99.47% [DEBUG] switch_core_media.c:5650 Audio Codec Compare [GSM:3:8000:20:13200:1]/[PCMU:0:8000:20:64000:1]
2022-02-19 13:04:04.027963 99.47% [DEBUG] switch_core_media.c:5650 Audio Codec Compare [GSM:3:8000:20:13200:1]/[PCMA:8:8000:20:64000:1]
2022-02-19 13:04:04.027963 99.47% [DEBUG] switch_core_media.c:5944 No 2833 in SDP. Liberal DTMF mode adding 101 as telephone-event.
2022-02-19 13:04:04.027963 99.47% [DEBUG] switch_core_media.c:5973 sofia/internal/[email protected]:5060 Set 2833 dtmf send payload to 101 recv payload to 101
2022-02-19 13:04:04.027963 99.47% [NOTICE] switch_channel.c:3993 Hangup sofia/internal/[email protected]:5060 [CS_EXECUTE] [INCOMPATIBLE_DESTINATION]

The hint to the cause of the error is above it – Codec comparison. If we look at the Audio Codec Compare lines, we can see the GSM codec we are trying to use, does not match the codecs configured in FreeSWITCH, hence getting the INCOMPATIBLE_DESTINATION error – None of the codecs offered match the codecs supported in FreeSWITCH.

So where do we go to fix this?

Well the SIP profile itself defines the codecs that are supported on this SIP profile,

FreeSWITCH SIP Profile (Sofia) codec settings

If you’re using a mostly default config, you’ll see this is set to a global variable, called $${global_codec_prefs}, so let’s take a look at vars.xml where this is defined:

FreeSWITCH default codec selection global variable

And there’s our problem, we need to add the GSM codec into that list to allow the calls,

So we change it to add the codecs we want to support, and reload the changes,

The Codec preferences I need for this IMS Application Server

Now when we want to make a call, success!

Successful call

FreeSWITCH as an IMS Application Server

After getting AMR support in FreeSWITCH I set about creating an IMS Application Server for VoLTE / IMS networks using FreeSWITCH.

So in IMS what is an Application Server? Well, the answer is almost anything that’s not a CSCF.

An Application Server could handle your Voicemail, recorded announcements, a Conference Factory, or help interconnect with other systems (without using a BGCF).

I’ll be using mine as a simple bridge between my SIP network and the IMS core I’ve got for VoLTE, with FreeSWITCH transcoding between AMR to PCMA.

Setting up FreeSWITCH

You’ll need to setup FreeSWITCH as per your needs, so that’s however you want to use it.

This post won’t cover setting up FreeSWITCH, there’s plenty of good resources out there for that.

The only difference is when you install FreeSWITCH, you will want to compile with AMR Support, so that you can interact with mobile phones using the AMR codec, which I’ve documented how to do here.

Setting up your IMS

In order to get calls from the IMS to the Application Server, we need a way of routing the calls to the Application Server.

There are two standards-compliant ways to achieve this,

The first is to use ENUM to route the calls you want to send to the Application Server, to the application server.

If you want to go down that path using Kamailio as your IMS I’ve got a post on that topic here.

But this is a blunt instrument, after all, it’ll only ever be used at the start of the call, what if we want to send it to an AS because a destination can’t be reached and we want to play back a recorded announcement?

Well that’s where iFCs come into the picture. Through the use of Initial Filter Criterias, we’re able to route different types of SIP traffic, requests and responses, based on our needs. Again we can do this in Kamailio, with a little help from an HSS like PyHSS.

Originating calls in FreeSWITCH

Through fs_cli you can orignate calls from FreeSWITCH.

At the CLI you can use the originate command to start a call, this can be used for everything from scheduled wake up calls, outbound call centers, to war dialing.

For example, what I’m using:

originate sofia/external/[email protected]:5061 61399999995 XML default
  • originate is the command on the FS_CLI
  • sofia/external/[email protected]:5061 is the call URL, with the application (I’m using mod_sofia, so sofia), the Sofia Profile (in my case external) and the SIP URI, or, if you have gateways configured, the to URI and the gateway to use.
  • 6139999995 is the Application
  • XML is the Dialplan to reference
  • default is the Context to use

But running this on the CLI is only so useful, we can use an ESL socket to use software to connect to FreeSWITCH’s API (Through the same mechanism fs_cli uses) in order to programmatically start calls.

But to do that first we need to expose the ESL API for inbound connections (Clients connecting to FreeSWITCH’s ESL API, which is different to FreeSWITCH connecting to an external ESL Server where FreeSWITCH is the client).

We’ll need to edit the event_socket.conf.xml file to define how this can be accessed:

<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <param name="nat-map" value="false"/>
    <param name="listen-ip" value="0.0.0.0"/>
    <param name="listen-port" value="8021"/>
    <param name="password" value="yoursecretpassword"/>
    <param name="apply-inbound-acl" value="lan"/>
    <param name="stop-on-bind-error" value="true"/>
  </settings>
</configuration>

Obviously you’ll need to secure this appropriately, good long password, and tight ACLs.

You may notice after applying these changes in the config, you’re no longer able to run fs_cli and access FreeSWITCH, this is because FreeSWITCH’s fs_cli tool connects to FreeSWITCH over ESL, and we’ve just changed tha parameters. You should still be able to connect by specifying the IP Address, port and the secret password we set:

fs_cli --host=10.0.1.16 --password=yoursecretpassword --port=8021

This also means we can run fs_cli from other hosts if permitted through the ACLs (kinda handy for managing larger clusters of FreeSWITCH instances).

But now we can also connect a remote ESL client to it to run commands like our Originate command to setup calls, I’m using GreenSwitch with ESL in Python:

import gevent
import greenswitch
import sys
#import Fonedex_TelephonyAPI
#sys.path.append('../WebUI/Flask/')
import uuid

import logging
logging.basicConfig(level=logging.DEBUG)


esl_server_host = "10.0.1.16"
logging.debug("Originating call to " + str(destination) + " from " + str(source))
logging.debug("Routing the call to " + str(dialplan_entry))
fs = greenswitch.InboundESL(host=str(esl_server_host), port=8021, password='yoursecretpassword')
  try:
      fs.connect()
      logging.debug("Connected to ESL server at " + str(esl_server_host))
  except:
      raise SystemError("Failed to connect to ESL Server at " + str(esl_server_host))

r = fs.send('bgapi originate {origination_caller_id_number=' + str(source) + '}sofia/external/' + str(destination) + '@10.0.1.252:5061 default XML')

And presto, a call is originated!

HTTP PUT call recordings from FreeSWITCH

The mod_httapi in FreeSWITCH allows you to upload your call recordings to a HTTP server, in my case I’ve put together a Flask based Python server for a project I’m working on, which when the call ends, uploads to my web server. Presto!

<action application="record" data="http://(file=recording.wav,name=part1.PCMU,method=POST)your_domain.com/recording_upload"/>

Obviously you’ll need to replace the URL etc, but you can then just extract the POSTed file out and boom, you don’t need to store any recordings on each FreeSWITCH instance.

This is fantastic if you’re running multiple instances in a cluster or containerized, and want every FreeSWITCH instance to be dumb and with access to the same data as every other instance.

Adding support for AMR Codec in FreeSWITCH

If you’re building IMS Networks, the AMR config is a must, but FreeSWITCH does not ship with AMR due to licencing constraints, but has all the hard work done, you just need to add the headers for AMR support and compile.

LibOpenCore has support for AMR which we build, and then with a few minor tweaks to copy the C++ header files over to the FreeSWITCH source directory, and enable support in modules.conf.

Then when building FreeSWITCH you’ve got the AMR Codec to enable you to manage IMS / VoLTE media streams from mobile devices.

Instead of copying and pasting a list of commands to do this, I’ve published a Dockerfile here you can use to build a Docker image, or on a straight Debian Buster machine if you’re working on VMs or Bare Metal if you run the commands from the Dockerfile on the VM / bare metal.

You can find the Dockerfile on my Github here,

FreeSWITCH WebRTC with sipML5

Most people think of SIP when it comes to FreeSWITCH, Asterisk and Kamailio, but all three support WebRTC.

FreeSWITCH makes WebRTC fairly easy to use and treats it much the same way as any SIP endpoint, in terms of registration and diaplan.

Setting up the SIP Profile

On the SIP profile we’ll need to activate WebRTC you’ll need to ensure a few lines of config are present:

    <!-- for sip over secure websocket support -->
    <!-- You need wss.pem in $${certs_dir} for wss or one will be created for you -->
    <param name="wss-binding" value=":7443"/>

Next you’ll need to restart FreeSWITCH and a self-signed certificate should get loaded,

Once you’ve restarted FreeSWITCH will fail to detect any WebSocket certificate and generate a self signed certificate for you. This means that you can verify FreeSWITCH is listening as expected using Curl:

curl https://yourhostname:7443 -vvv

You should see an error regarding the connection failing due to an invalid certificate, if so, great! Let’s put in a valid certificate.

If not double check the firewall on your server allow traffic to port TCP 7443,

Loading your TLS Certificate

WebRTC & websocket are recent standards – this means a valid TLS certificate is mandatory. So to get this to work you’ll need a valid SSL certificate.

Let’sEncrypt should work fine, if you’ve got your own CA that’s in the trusted CA list on your machine that’ll do, or I’m using a cert I generated with Mkcert.

When we restarted FreeSWITCH after adding the wss-binding config a certificate was automatically generated in the $${certs_dir} of FreeSWITCH,

You can verify where the certs_dir is by echoing out the variable in FreeSWITCH:

fs_cli -x 'eval $${certs_dir}'

Unless you’ve changed it you’ll probably find your certs in /etc/freeswitch/tls/

The certificate and private key are stored in a single file, with the Certificate and the Private Key appended to the end,

In my case the certificate is called “webrtc.pem” and the private key file is “webrtc-key.pem”,

I’ll need to start by replacing the contents of the current certificate/ key file wss.pem with the certificate I’ve got webrtc.pem, and then appending the private key – webrtc-key.pem to the end of wss.pem,

cat /home/nick/webrtc.pem > /etc/freeswitch/tls/wss.pem
cat /home/nick/webrtc-key.pem >> /etc/freeswitch/tls/wss.pem

Next up I’ll restart FreeSWITCH, and run Curl again to verify this time the certificate is valid:

curl https://yourhostname:7443 -vvv

All going well no certificate error will be reported and we can setup our WebRTC client.

Configuring sipML5

Dubango Telecom’s sipML5 is a BSD licenced HTML5 SIP client,

I’ll use the demo version on their website to connect to my FreeSWITCH WebRTC server, which you can run in your browser from here,

We’ll start by clicking the “Export Mode” button to set our wss:// URL;

Change the WebSocket Server URL to the URL of your FreeSWITCH instance (you must use a domain, not an IP Address)

If you’re running behind a NAT adding ICE servers is probably a good idea, although this will slow down connection times, you can use Google’s public STUN server by pasting in the below value:

[{ url: 'stun:stun.l.google.com:19302'}]

Finally we’ll save those settings and return back to the main tab,

You’ll need to register with a username and password that’s valid on the FreeSWITCH box, in my case I’m using 1000 with the password 1000 (exists by default),

Replace webrtc with the domain name of your FreeSWITCH instance,

Finally you should be able to click Login and see Connected above,

Then we can make calls to endpoints on FreeSWITCH using the dial box;

The Debug console in your browser will provide all the info you need to debug any issues, and you can trace WebSocket traffic using Sofia like any other SIP traffic.

Hopefully this was useful to you – I’ll cover more of WebRTC on Asterisk and also Kamailio in later posts!

FreeSWITCH + ESL = Programmable Voice

No great secret, I’m a big Python fan.

Recently I’ve been working on a few projects with FreeSWITCH, and looking at options for programmatically generating dialplans, instead of static XML files.

Why not Static XML?

So let’s say I define a static XML dialplan.

It works great, but if I want to change the way a call routes I need to do it from the dialplan,

That’s not ideal if you’re using a distributed cluster of FreeSWITCH instances, or if you want to update on the fly.

Static XML means we have to define our dialplan when setting up the server, and would have to reconfigure the server to change it.

So what about mod_xml_curl?

When I’ve done this in the past I’ve relied on the mod_xml_curl module.

mod_xml_curl gets the XML dialplan using Curl from a web server, and so you setup a web server using Flask/PHP/etc, and dynamically generate the dialplan when the call comes in.

This sounds good, except you can’t update it on the fly.

mod_xml_curl means call routing decisions are made at the start of the call, and can’t be changed midway through the call.

So what’s ESL?

ESL is the Event Socket Library, essentially a call comes in, an ESL request is made to an external server.

For each step in the dialplan, an ESL request will be sent to the external server which tells it to do,

ESL allows us to use all FreeSWITCH’s fantastic modules, without being limited as to having to perform the call routing logic in FreeSWITCH.

So how do I use ESL?

You’ll need two create an ESL server,

Luckily there’s premade examples for popular languages;

Once you’ve got a basic server defined we’ll need to put some config in our XML Dialplan to say “transfer all your thinking to ESL!”;

<!-- Send everything that's numbers to ESL for Processing --> 
    <extension name="esl_route">
      <condition field="destination_number" expression="^\d*$">
        <action application="socket" data="10.0.1.252:5000"/>
        <action application="log" data="ERR Made it past ESL - Play error."/>
        <action application="playback" data="ivr/ivr-call_cannot_be_completed_as_dialed"/>
      </condition>
    </extension>

In the above example my ESL server is on 10.0.1.252 / port 5000.

Now any calls coming in will be transfered to ESL and where it goes from there, is something you define in your prefered programming language.

In the next post I’ll cover how I’ve been addressing this using Python and Greenswitch.