Category Archives: VoIP

Failures in cobbling together a USSD Gateway

One day recently I was messing with the XCAP server, trying to set the Call Forward timeout. In the process I triggered the UE to send a USSD request to the IMS.

Huh, I thought, “I wonder how hard it would be to build a USSD Gateway for our IMS?”, and this my friends, is the story of how I wasted a good chunk of my weekend trying (and failing) to add support for USSD.

You might be asking “Who still uses USSD?” – The use cases for USSD are pretty thin on the ground in this day and age, but I guess balance query, and uh…

But this is the story of what I tried before giving up and going outside…

Routing

First I’d need to get the USSD traffic towards the USSD Gateway, this means modifying iFCs. Skimming over the spec I can see the Recv-Info: header for USSD traffic should be set to “g.3gpp.ussd” so I knocked up an iFC to match that, and route the traffic to my dev USSD Gateway, and added it to the subscriber profile in PyHSS:

  <!-- SIP USSD Traffic to USSD-GW-->
        <InitialFilterCriteria>
            <Priority>25</Priority>
            <TriggerPoint>
                <ConditionTypeCNF>1</ConditionTypeCNF>
                <SPT>
                    <ConditionNegated>0</ConditionNegated>
                    <Group>1</Group>
                    <SIPHeader>
                      <Header>Recv-Info</Header>
                      <Content>"g.3gpp.ussd"</Content>
                    </SIPHeader>
                </SPT>                
            </TriggerPoint>
            <ApplicationServer>
                <ServerName>sip:ussdgw:5060</ServerName>
                <DefaultHandling>0</DefaultHandling>
            </ApplicationServer>
        </InitialFilterCriteria>

Easy peasy, now we have the USSD requests hitting our USSD Gateway.

The Response

I’ll admit that I didn’t jump straight to the TS doc from the start.

The first place I headed was Google to see if I could find any PCAPs of USSD over IMS/SIP.

And I did – Restcomm seems to have had a USSD product a few years back, and trawling around their stuff provided some reference PCAPs of USSD over SIP.

So the flow seemed pretty simple, SIP INVITE to set up the session, SIP INFO for in-dialog responses and a BYE at the end.

With all the USSD guts transferred as XML bodies, in a way that’s pretty easy to understand.

Being a Kamailio fan, that’s the first place I started, but quickly realised that SIP proxies, aren’t great at acting as the UAS.

So I needed to generate in-dialog SIP INFO messages, so I turned to the UAC module to generate the SIP INFO response.

My Kamailio code is super simple, but let’s have a look:

request_route {

        xlog("Request $rm from $fU");

        if(is_method("INVITE")){
                xlog("USSD from $fU to $rU (Emergency number) CSeq is $cs ");
                sl_reply("200", "OK Trying USSD Phase 1");      #Generate 200 OK
                route("USSD_Response"); #Call USSD_Response route block
                exit;
        }
}

route["USSD_Response"]{
        xlog("USSD_Response Route");
        #Generate a new UAC Request
        $uac_req(method)="INFO";
        $uac_req(ruri)=$fu;     #Copy From URI to Request URI
        $uac_req(furi)=$tu;     #Copy To URI to From URI
        $uac_req(turi)=$fu;     #Copy From URI to To URI
        $uac_req(callid)=$ci;   #Copy Call-ID
                                #Set Content Type to 3GPP USSD
        $uac_req(hdrs)=$uac_req(hdrs) + "Content-Type: application/vnd.3gpp.ussd+xml\r\n";
                                #Set the USSD XML Response body
        $uac_req(body)="<?xml version='1.0' encoding='UTF-8'?>
        <ussd-data>
                <language value=\"en\"/>
                <ussd-string value=\"Bienvenido. Seleccione una opcion: 1 o 2.\"/>
        </ussd-data>";
        $uac_req(evroute)=1;    #Set the event route to use on return replies
        uac_req_send();         #Send it!
}

So the UAC module generates the 200 OK and sends it back.

“That was quick” I told myself, patting myself on the back before trying it out for the first time.

Huston, we have a problem – Although the Call-ID is the same, it’s not an in-dialog response as the tags aren’t present, this means our UE send back a 405 to the SIP INFO.

Right. Perhaps this is the time to read the Spec…

Okay, so the SIP INFO needs to be in dialog. Can we do that with the UAC module? Perhaps not…

But the Transaction Module ™ in Kamailio exposes and option on the ctl API to generate an in-dialog UAC – this could be perfect…

But alas real life came back to rear its ugly head, and this adventure will have to continue another day…

Update: Thanks to a kindly provided PCAP I now know what I was doing wrong, and so we’ll soon have a follow up to this post named “Successes in cobbling together a USSD Gateway” just as soon as I have a weekend free.

Kamailio Bytes: Adding Prometheus + Grafana to Kamailio

I recently fell in love with the Prometheus + Grafana combo, and I’m including it in as much of my workflow as possible, so today we’ll be integrating this with another favorite – Kamailio.

Why would we want to integrate Kamailio into Prometheus + Grafana? Observability, monitoring, alerting, cool dashboards to make it look like you’re doing complicated stuff, this duo have it all!

I’m going to assume some level of familiarity with Prometheus here, and at least a basic level of understanding of Kamailio (if you’ve never worked with Kamailio before, check out my Kamailio 101 Series, then jump back here).

So what will we achieve today?

We’ll start with the simple SIP Registrar in Kamailio from this post, and we’ll add on the xhttp_prom module, and use it to expose some stats on the rate of requests, and responses sent to those requests.

So to get started we’ll need to load some extra modules, xhttp_prom module requires xhttp (If you’d like to learn the basics of xhttp there’s also a Kamailio Bytes – xHTTP Module post covering the basics) so we’ll load both.

xHTTP also has some extra requirements to load, so in the top of our config we’ll explicitly specify what ports we want to bind to, and set two parameters that control how Kamailio handles HTTP requests (otherwise you’ll not get responses for HTTP GET requests).

listen=tcp:0.0.0.0:9090
listen=tcp:0.0.0.0:5060
listen=udp:0.0.0.0:5060

http_reply_parse=yes
tcp_accept_no_cl=yes

Then where you load all your modules we’ll load xhttp and xhttp_prom, and set the basic parameters:

loadmodule "xhttp.so"
loadmodule "xhttp_prom.so"

# Define two counters and a gauge
modparam("xhttp_prom", "xhttp_prom_stats", "all")

By setting xhttp_prom module to expose all stats, this exposes all of Kamailio’s internal stats as counters to Prometheus – This means we don’t need to define all our own counters / histograms / gauges, instead we can use the built in ones from Kamailio. Of course we can define our own custom ones, but we’ll do that in our next post.

Lastly we’ll need to add an event route to handle HTTP requests to the /metrics URL:

event_route[xhttp:request] {
	xlog("Got a request!");
	xlog("$ru");
	$var(xhttp_prom_root) = $(hu{s.substr,0,8});
	if ($var(xhttp_prom_root) == "/metrics") {
			xlog("Called metrics");
			prom_dispatch();
			xlog("prom_dispatch() called");
			return;
	} else
		xhttp_reply("200", "OK", "text/html",
        		"<html><body>Wrong URL $hu</body></html>");
}

Restart, and browse to the IP of your Kamailio instance (mine is 10.01.23) port 9090 /metrics and you’ll get something like this:

Kamailio metrics endpoing used by Prometheus

That my friends, is the sort of data that Prometheus gobbles up, so let’s point Prometheus at it and see what data we get back.

Over on my Prometheus server I’ve edited /etc/prometheus/prometheus.yml to target our new Prometheus endpoint.

  - job_name: "kamailo"
    static_configs:
      - targets: ["10.0.1.23:9090"]  
    honor_timestamps: false

So how can we see this data? Well first off if we log into Prometheus we can see the data flowing in:

If we throw some SIP REGISTER traffic at our Kamailio instance and check on the kamailio_registrar_accepted_regs stat we can see our registrations.

After a few clicks in Grafana we can run some graphs for this data too:

So that’s it, Kamailio’s core stats are now exposed to Prometheus, and we can render this information in Grafana.

There’s a copy of the full code used here available in the Github, and in our next post we’ll look at defining our own metrics in Kamailio and then interacting with them.

Demystifying SS7 & Sigtran – Part 6 – Calling with ISUP

So far in our lab we’ve got connectivity between to points, but we’re not carrying any useful data on top of it.

In the same way that TCP is great, but what makes it really useful is carrying application layers like HTTP on top, MTP3 exists to facilitate carrying higher-layer protocols, like ISUP, MAP, SCCP, etc, so let’s get some traffic onto our network.

ISUP is the ISDN User Part, ISUP is used to setup and teardown calls between two exchanges / SSPs – it’s the oldest and the most simple SS7 application to show off, so that’s what we’ll be working with today.

If you’ve not dealt much with ISDN in the past, then that’s OK – we’re not going to deep dive into all the nitty gritty of how ISDN Signaling works, but we’ll just skim the surface to showing how SS7/Sigtran transports the ISUP packets. So you can see how SS7 is used to transport this protocol.

Very Basic ISDN Signalling

ISUP is used to setup and teardown calls between telephone exchanges, which in SS7 networks, are the Service Switching Points (SSPs) we talked about in this post.

You can think of it a lot like SIP, which is if not the child of ISUP, then it at least bares a striking resemblance.

So let’s look at an ISUP call flow:

The call is initiated with an Initial Address Message (IAM), akin to a SIP INVITE, sent by the SSP/Exchange of the calling party to the SSP/Exchange of the called party.
When the remote party starts to ring, the remote exchange sends an Address Complete (ACM), which is similar to a 100 TRYING in SIP.
Once the remote party answers, the remote exchange sends back an Answer Message (ANM), and our call starts, just like a 200 OK.

Rather than SDP for transferring media, timeslots or predefined channels / circuits are defined, each identified by a number, which both sides will use for the media path.

Finally whichever side terminates the call will send a Release (REL) message, which is confirmed with the Release Complete (RLC).

I told you we’d be quick!

So that’s the basics of ISUP, in our next post we’ll do some PCAP analysis on real world ISUP flows!

Kamailio Diameter Routing Agent Support

Recently I’ve been working on open source Diameter Routing Agent implementations (See my posts on FreeDiameter).

With the hurdles to getting a DRA working with open source software covered, the next step was to get all my Diameter traffic routed via the DRAs, however I soon rediscovered a Kamailio limitation regarding support for Diameter Routing Agents.

You see, when Kamailio’s C Diameter Peer module makes a decision as to where to route a request, it looks for the active Diameter peers, and finds a peer with the suitable Vendor and Application IDs in the supported Applications for the Application needed.

Unfortunately, a DRA typically only advertises support for one application – Relay.

This means if you have everything connected via a DRA, Kamailio’s CDP module doesn’t see the Application / Vendor ID for the Diameter application on the DRA, and doesn’t route the traffic to the DRA.

The fix for this was twofold, the first step was to add some logic into Kamailio to determine if the Relay application was advertised in the Capabilities Exchange Request / Answer of the Diameter Peer.

I added the logic to do this and exposed this so you can see if the peer supports Diameter relay when you run “cdp.list_peers”.

With that out of the way, next step was to update the routing logic to not just reject the candidate peer if the Application / Vendor ID for the required application was missing, but to evaluate if the peer supports Diameter Relay, and if it does, keep it in the game.

I added this functionality, and now I’m able to use CDP Peers in Kamailio to allow my P-CSCF, S-CSCF and I-CSCF to route their traffic via a Diameter Routing Agent.

I’ve got a branch with the changes here and will submit a PR to get it hopefully merged into mainline soon.

Filtering for 3GPP DNS in Wireshark

If you work with IMS or Packet Core, there’s a good chance you need DNS to work, and it doesn’t always.

When I run traces, I’ve always found I get swamped with DNS traffic, UE traffic, OS monitoring, updates, etc, all combine into a big firehose – while my Wireshark filters for finding EPC and IMS traffic is pretty good, my achilles heel has always been filtering the DNS traffic to just get the queries and responses I want out of it.

Well, today I made that a bit better.

By adding this to your Wireshark filter:

dns contains 33:67:70:70:6e:65:74:77:6f:72:6b:03:6f:72:67:00

You’ll only see DNS Queries and Responses for domains at the 3gppnetwork.org domain.

This makes my traces much easier to read, and hopefully will do the same for you!

Bonus, here’s my current Wireshark filter for working EPC/IMS:

(diameter and diameter.cmd.code != 280) or  (sip and !(sip.Method == "OPTIONS") and !(sip.CSeq.method == "OPTIONS")) or (smpp and (smpp.command_id != 0x00000015 and smpp.command_id != 0x80000015)) or (mgcp and !(mgcp.req.verb == "AUEP") and !(mgcp.rsp.rspcode == 500)) or isup or sccp or rtpevent or s1ap or gtpv2 or pfcp or (dns contains 33:67:70:70:6e:65:74:77:6f:72:6b:03:6f:72:67:00)

Cisco ITP / SS7 STP – Viewing MTP3 traffic from TDM Links

Okay, so a little late to the party on this one…

The other day I had to setup a TDM (E1) based SS7 link (oh yes my friend – they still exist) to interconnect with another operator.

I’m using Cisco’s ITP product as the STP / Signaling Gateway, and my trusty port mirror for what’s going on doesn’t extend down to TDM links.

But I found out you can mirror MTP3 traffic from TDM links in the STP!

Firstly we’ve got to define the remote destination to send the TDM mirrored traffic to, and an access list to match all SS7 traffic:

cs7 paklog your.ip.for.sniffing dest-port 514
access-list 2700 instance 0 permit all

Next up we start a debug session for traffic matching that access list:

debug cs7 mtp3 paklog 2700

And then over on your monitoring box (the IP you specified in your.ip.for.sniffing ) fire up Wireshark and voila!

All our MTP3 traffic!

This was super useful for ensuring the ITP was working correctly as a signaling gateway and passing the M3UA packets over onto MTP3 MSUs.

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 in Baby Steps – Part 3 – RatingProfiles & RatingPlans

In our last post we introduced the CGrateS API and we used it to add Rates, Destinations and define DestinationRates.

In this post, we’ll create the RatingPlan that references the DestinationRate we just defined, and the RatingProfile that references the RatingPlan, and then, as the cherry on top – We’ll rate some calls.

For anyone looking at the above diagram for the first time, you might be inclined to ask why what is the purpose of having all these layers?

This layered architecture allows all sorts of flexibility, that we wouldn’t otherwise have, for example, we can have multiple RatingPlans defined for the same Destinations, to allow us to have different Products defined, with different destinations and costs.

Likewise we can have multiple RatingProfiles assigned for the same destinations to allow us to generate multiple CDRs for each call, for example a CDR to bill the customer with and a CDR with our wholesale cost.

All this flexibility is enabled by the layered architecture.

Define RatingPlan

Picking up where we left off having just defined the DestinationRate, we’ll need to create a RatingPlan and link it to the DestinationRate, so let’s check on our DestinationRates:

print("GetTPRatingProfileIds: ")
TPRatingProfileIds = CGRateS_Obj.SendData({"jsonrpc": "2.0", "method": "ApierV1.GetRatingProfileIDs", "params": [{"TPid": "cgrates.org"}]})
print("TPRatingProfileIds: ")
pprint.pprint(TPRatingProfileIds)

From the output we can see we’ve got the DestinationRate defined, there’s a lot of info returned (I’ve left out most of it), but you can see the Destination, and the Rate associated with it is returned:

OrderedDict([('id', 1),
             ('result',
              OrderedDict([('TPid', 'cgrates.org'),
                           ('ID', 'DestinationRate_AU'),
                           ('DestinationRates',
                            [OrderedDict([('DestinationId', 'Dest_AU_Fixed'),
                                          ('RateId', 'Rate_AU_Fixed_Rate_1'),
                                          ('Rate', None),
                                          ('RoundingMethod', '*up'),
                                          ('RoundingDecimals', 4),
                                          ('MaxCost', 0),
                                          ('MaxCostStrategy', '')]),
                             OrderedDict([('DestinationId', 'Dest_AU_Mobile'),
                                          ('RateId', 'Rate_AU_Mobile_Rate_1'),
                                          ('Rate', None),
                                          ...

So after confirming that our DestinationRates are there, we’ll create a RatingPlan to reference it, for this we’ll use the APIerSv1.SetTPRatingPlan API call.

TPRatingPlans = CGRateS_Obj.SendData({
    "id": 3,
    "method": "APIerSv1.SetTPRatingPlan",
    "params": [
        {
            "TPid": "cgrates.org",
            "ID": "RatingPlan_VoiceCalls",
            "RatingPlanBindings": [
                {
                    "DestinationRatesId": "DestinationRate_AU",
                    "TimingId": "*any",
                    "Weight": 10
                }
            ]
        }
    ]
})

RatingPlan_VoiceCalls = CGRateS_Obj.SendData(
    {"jsonrpc": "2.0", "method": "ApierV1.GetTPRatingPlanIds", "params": [{"TPid": "cgrates.org"}]})
print("RatingPlan_VoiceCalls: ")
pprint.pprint(RatingPlan_VoiceCalls)
print("\n\n\n")

In our basic example, this really just glues the DestinationRate_AU object to RatingPlan_VoiceCalls.

It’s worth noting that you can use a RatingPlan to link to multiple DestinationRates, for example, we might want to have a different RatingPlan for each region / country, we can do that pretty easily too, in the below example I’ve referenced other Destination Rates (You’d go about defining the DestinationRates for these other destinations / rates the same way as we did in the last example).

{
    "id": 3,
    "method": "APIerSv1.SetTPRatingPlan",
    "params": [
        {
            "TPid": "cgrates.org",
            "ID": "RatingPlan_VoiceCalls",
            "RatingPlanBindings": [
                {
                    "DestinationRatesId": "DestinationRate_USA",
                    "TimingId": "*any",
                    "Weight": 10
                },
                    "DestinationRatesId": "DestinationRate_UK",
                    "TimingId": "*any",
                    "Weight": 10
                },
                    "DestinationRatesId": "DestinationRate_AU",
                    "TimingId": "*any",
                    "Weight": 10
                },
                ...

One last step before we can test this all end-to-end, and that’s to link the RatingPlan we just defined with a RatingProfile.

StorDB & DataDB

Psych! Before we do that, I’m going to subject you to learning about backends for a while.

So far we’ve skirted around CGrateS architecture, but this is something we need to know for now.

To keep everything fast, a lot of data is cached in what is called a DataDB (if you’ve followed since part 1, then your DataDB is Redis, but there are other options).

To keep everything together, databases are used for storage, called StorDB (in our case we are using MySQL, but again, we can have other options) but calls to this database are minimal to keep the system fast.

If you’re an astute reader, you may have noticed many of our API calls have TP in method name, if the API call has TP in the name, it is storing it in the StoreDB, if it doesn’t, it means it’s storing it only in DataDB.

Why does this matter? Well, let’s look a little more closely and it will become clear:

ApierV1.SetRatingProfile will set the data only in DataDB (Redis), because it’s in the DataDB the change will take effect immediately.

ApierV1.SetTPRatingProfile will set the data only in StoreDB (MySQL), it will not take effect until it is copied from the database (StoreDB) to the cache (DataDB).

To do this we need to run:

cgr-console "load_tp_from_stordb Tpid=\"cgrates.org\" Cleanup=true Validate=true DisableDestinations=false"

Which pulls the data from the database into the cache, as you may have guessed there’s also an API call for this:

{"method":"APIerSv1.LoadTariffPlanFromStorDb","params":[{"TPid":"cgrates.org","DryRun":False,"Validate":True,"APIOpts":None,"Caching":None}],"id":0}

After we define the RatingPlan, we need to run this command prior to creating the RatingProfile, so it has something to reference, so we’ll do that by adding:

print(CGRateS_Obj.SendData({"method":"APIerSv1.LoadTariffPlanFromStorDb","params":[{"TPid":"cgrates.org","DryRun":False,"Validate":True,"APIOpts":None,"Caching":None}],"id":0}))

Now, on with the show!

Defining a RatingProfile

The last piece of the puzzle to define is the RatingProfile.

We define a few key things in the rating profile:

  • The Tenant – CGrateS is multitenant out of the box (in our case we’ve used tenant named “cgrates.org“, but you could have different tenants for different customers).
  • The Category – As we covered in the first post, CGrateS can bill voice calls, SMS, MMS & Data consumption, in this scenario we’re billing calls so we have the value set to *call, but we’ve got many other options.
  • The Subject – This is loosely the Source / Calling Party; in our case we’re using a wildcard value *any which will match any Subject
  • The RatingPlanActivations list the RatingPlanIds of the RatingPlans this RatingProfile uses

So let’s take a look at what we’d run to add this:

#Reload data from StorDB
print(CGRateS_Obj.SendData({"method":"APIerSv1.LoadTariffPlanFromStorDb","params":[{"TPid":"cgrates.org","DryRun":False,"Validate":True,"APIOpts":None,"Caching":None}],"id":0}))

#Create RatingProfile
print(CGRateS_Obj.SendData({
    "method": "APIerSv1.SetRatingProfile",
    "params": [
        {
            "TPid": "RatingProfile_VoiceCalls",
            "Overwrite": True,
            "LoadId" : "APItest",
            "Tenant": "cgrates.org",
            "Category": "call",
            "Subject": "*any",
            "RatingPlanActivations": [
                {
                    "ActivationTime": "2014-01-14T00:00:00Z",
                    "RatingPlanId": "RatingPlan_VoiceCalls",
                    "FallbackSubjects": ""
                }
            ]
        }
    ]
}))

print("GetTPRatingProfileIds: ")
TPRatingProfileIds = CGRateS_Obj.SendData({"jsonrpc": "2.0", "method": "ApierV1.GetRatingProfileIDs", "params": [{"TPid": "cgrates.org"}]})
print("TPRatingProfileIds: ")
pprint.pprint(TPRatingProfileIds)

Okay, so at this point, all going well, we should have some data loaded, we’ve gone through all those steps to load this data, so now let’s simulate a call to a Mobile Number (22c per minute) for 123 seconds.

We can do this from the CLI:

cgr-console 'cost Category="call" Tenant="cgrates.org" Subject="1001" Destination="6140000" AnswerTime="2025-08-04T13:00:00Z" Usage="123s"'

We should get the cost back of 66 cents, as 3x 22 cents.

Call showing 66 cent cost

If that’s worked, breath a sigh of relief. That’s the worst done.*

As you may have guessed we can also check this through API calls,

print("Testing call..")
cdr = CGRateS_Obj.SendData({"method": "APIerSv1.GetCost", "params": [ { \
    "Tenant": "cgrates.org", \
    "Category": "call", \
    "Subject": "1001", \
    "AnswerTime": "2025-08-04T13:00:00Z", \
    "Destination": "6140000", \
    "Usage": "123s", \
    "APIOpts": {}
    }], "id": 0})
pprint.pprint(cdr)

And you should get the same output.

If you’ve had issues with this, I’ve posted a copy of the code in GitHub.

We’re done here. Well done. This one was a slog.

CGrateS in Baby Steps – Part 2 – Adding Rates and Destinations through the API

In our last post we dipped a toe into CGrateS.

We cheated a fair bit, to show something that worked, but it’s not something you’d probably want to use in real life, loading static CSV files gets us off the ground, but in reality we don’t want to manage a system through CSV files.

Instead, we’d want to use an API.

Fair warning – There is some familiarity expected with JSON and RESTful APIs required, we’ll use Python3 for our examples, but you can use any programing language you’re comfortable with, or even CURL commands.

So we’re going to start by clearing out all the data we setup in CGrateS using the cgr-loader tool from those imported CSVs:

redis-cli flushall
sudo mysql -Nse 'show tables' cgrates | while read table; do sudo mysql -e "truncate table $table" cgrates; done
cgr-migrator -exec=*set_versions -stordb_passwd=CGRateS.org
sudo systemctl restart cgrates

So what have we just done?
Well, we’ve just cleared all the data in CGrateS.
We’re starting with a blank slate.

In this post, we’re going to define some Destinations, some Rates to charge and then some DestinationRates to link each Destination to a Rate.

But this time we’ll be doing this through the CGrateS API.

Introduction to the CGrateS API

CGrateS is all API driven – so let’s get acquainted with this API.

I’ve written a simple Python wrapper you can find here that will make talking to CGRateS a little easier, so let’s take it for a spin and get the Destinations that are loaded into our system:

import cgrateshttpapi
CGRateS_Obj = cgrateshttpapi.CGRateS('172.16.41.133', 2080) #Replace this IP with the IP Address of your CGrateS instance...

destinations = CGRateS_Obj.SendData({'method':'ApierV1.GetTPDestinationIDs','params':[{"TPid":"cgrates.org"}]})['result']

#Pretty print the result:
print("Destinations: ")
pprint.pprint(destinations)

All going well you’ll see something like this back:

Initializing with host 172.16.41.133 on port 2080
Sending Request with Body:
{'method': 'ApierV2.Ping', 'params': [{'Tenant': 'cgrates.org'}]}
Sending Request with Body:
{'method': 'ApierV2.GetTPDestinationIDs', 'params': [{"TPid":"cgrates.org"}]}
Destinations from CGRates: []

So what did we just do?
Well, we sent a JSON formatted string to the CGRateS API at 172.16.41.133 on port 2080 – You’ll obviously need to change this to the IP of your CGrateS instance.

In the JSON body we sent we asked for all the Destinations using the ApierV1.GetTPDestinationIDs method, for the TPid ‘cgrates.org’,

And it looks like no destinations were sent back, so let’s change that!

Note: There’s API Version 1 and API Version 2, not all functions exist in both (at least not in the docs) so you have to use a mix.

Adding Destinations via the API

So now we’ve got our API setup, let’s see if we can add a destination!

To add a destination, we’ll need to go to the API guide and find the API call to add a destination – in our case the API call is ApierV2.SetTPDestination and will look like this:

{'method': 'ApierV2.SetTPDestination', 'params': [
    {"TPid": "cgrates.org", "ID": "Dest_AU_Mobile",
        "Prefixes": ["614"]}]}

So we’re creating a Destination named Dest_AU_Mobile and Prefix 614 will match this destination.

Note: I like to prefix all my Destinations with Dest_, all my rates with Rate_, etc, so it makes it easy when reading what’s going on what object is what, you may wish to do the same!

So we’ll use the Python code we had before to list the destinations, but this time, we’ll use the ApierV2.SetTPDestination API call to add a destination before listing them, let’s take a look:

import cgrateshttpapi
import pprint
import sys
CGRateS_Obj = cgrateshttpapi.CGRateS('172.16.41.133', 2080)

CGRateS_Obj.SendData({'method':'ApierV2.SetTPDestination','params':[{"TPid":"cgrates.org","ID":"Dest_AU_Mobile","Prefixes":["614"]}]})

destinations = CGRateS_Obj.SendData({'method':'ApierV1.GetTPDestinationIDs','params':[{"TPid":"cgrates.org"}]})['result']
print("Destinations: ")
pprint.pprint(destinations)
print("\n\n\n")

Now if you run the code you’ll see something like this:

Initializing with host 172.16.41.133 on port 2080
Sending Request with Body:

Sending Request with Body:
{'method': 'ApierV2.SetTPDestination', 'params': [{'TPid': 'cgrates.org', 'ID': 'Dest_AU_Mobile', 'Prefixes': ['614']}]}

{'method': 'ApierV1.GetTPDestinationIDs', 'params': [{'TPid': 'cgrates.org'}]}
Destinations: 
['Dest_AU_Mobile']

Boom! There’s our added destination, le’s add a few more using the same process, so we’ve got a few other destinations defined:

CGRateS_Obj = cgrateshttpapi.CGRateS('172.16.41.133', 2080)

CGRateS_Obj.SendData({'method':'ApierV2.SetTPDestination','params':[{"TPid":"cgrates.org","ID":"Dest_AU_Fixed","Prefixes":["612", "613", "617", "618"]}]})
CGRateS_Obj.SendData({'method':'ApierV2.SetTPDestination','params':[{"TPid":"cgrates.org","ID":"Dest_AU_Mobile","Prefixes":["614"]}]})
CGRateS_Obj.SendData({'method':'ApierV2.SetTPDestination','params':[{"TPid":"cgrates.org","ID":"Dest_AU_TollFree","Prefixes":["6113", "6118"]}]})



print("Destinations: ")
for destination in destinations:
    destination = CGRateS_Obj.SendData({'method':'ApierV1.GetTPDestination','params':[{"TPid":"cgrates.org", "ID" : str(destination)}]})['result']
    pprint.pprint(destination)
print("\n\n\n")
sys.exit()

After adding some prettier printing and looping through all the destinations, here’s what your destinations should look like:

OrderedDict([('TPid', 'cgrates.org'),
             ('ID', 'Dest_AU_Fixed'),
             ('Prefixes', ['612', '613', '617', '618'])])

OrderedDict([('TPid', 'cgrates.org'),
             ('ID', 'Dest_AU_Mobile'),
             ('Prefixes', ['614'])])

OrderedDict([('TPid', 'cgrates.org'),
             ('ID', 'Dest_AU_TollFree'),
             ('Prefixes', ['6113', '6118'])])

Notice for AU Fixed, we defined multiple prefixes under the same Destination? Just as items in the list.

So we’ve created a bunch of Destinations, like so:

NamePrefix
Dest_AU_TollFree6113 & 6118
Dest_AU_Fixed612, 613, 617 & 618
Dest_AU_Mobile614
Destinations we just created

Next let’s create some rates which we can then associate with these destinations.

Adding Rates via the API

So to begin with let’s see if we’ve got any rates defined, we can do this with another API call, this time the ApierV1.GetTPRateIds call.

{"method":"ApierV1.GetTPRateIds","params":[{"TPid":"cgrates.org"}]}

And at the moment that returns no results, so let’s add some rates.

For this we’ll use the ApierV1.SetTPRate function:

{"method":"ApierV1.SetTPRate","params":[{"ID":"Rate_AU_Mobile_Rate_1","TPid":"cgrates.org","RateSlots":[{"ConnectFee":0,"Rate":22,"RateUnit":"60s","RateIncrement":"60s","GroupIntervalStart":"0s"}]}],"id":1}

If we post this to the CGR engine, we’ll create a rate, named Rate_AU_Mobile_Rate_1 that bills 22 cents per minute, charged every 60 seconds.

Let’s add a few rates:

CGRateS_Obj.SendData({"method":"ApierV1.SetTPRate","params":[{"ID":"Rate_AU_Mobile_Rate_1","TPid":"cgrates.org","RateSlots":[{"ConnectFee":0,"Rate":22,"RateUnit":"60s","RateIncrement":"60s","GroupIntervalStart":"0s"}]}],"id":1})
CGRateS_Obj.SendData({"method":"ApierV1.SetTPRate","params":[{"ID":"Rate_AU_Fixed_Rate_1","TPid":"cgrates.org","RateSlots":[{"ConnectFee":0,"Rate":14,"RateUnit":"60s","RateIncrement":"60s","GroupIntervalStart":"0s"}]}],"id":1})
CGRateS_Obj.SendData({"method":"ApierV1.SetTPRate","params":[{"ID":"Rate_AU_Toll_Free_Rate_1","TPid":"cgrates.org","RateSlots":[{"ConnectFee":25,"Rate":0,"RateUnit":"60s","RateIncrement":"60s","GroupIntervalStart":"0s"}]}],"id":1})

TPRateIds = CGRateS_Obj.SendData({"method":"ApierV1.GetTPRateIds","params":[{"TPid":"cgrates.org"}]})['result']
print(TPRateIds)
for TPRateId in TPRateIds:
    print("\tRate: " + str(TPRateId))

All going well, when you add the above, we’ll have added 3 new rates:

Rate NameCost
Rate_AU_Fixed_Rate_114c per minute charged every 60s
Rate_AU_Mobile_Rate_122c per minute charged every 60s
Rate_AU_Toll_Free_Rate_125c connection, untimed
Rates we just created

Linking Rates to Destinations

So now with Destinations defined, and Rates defined, it’s time to link these two together!

Destination Rates link our Destinations and Route rates, this decoupling means that we can have one Rate shared by multiple Destinations if we wanted, and makes things very flexible.

For this example, we’re going to map the Destinations to rates like this:

DestinationRate NameDestination NameRate Name
DestinationRate_AUDest_AU_FixedRate_AU_Fixed_Rate_1
DestinationRate_AUDest_AU_MobileRate_AU_Mobile_Rate_1
DestinationRate_AUDest_AU_TollFreeRate_AU_Toll_Free_Rate_1
Destination_Rate_AU we will create

So let’s go about making this link in CGrateS, for this we’ll use the ApierV1.SetTPDestinationRate method to add the DestinationRate, and the ApierV1.GetTPDestinationRateIds to get the list of them.

CGRateS_Obj.SendData({"method": "ApierV1.SetTPDestinationRate", "params": \
    [{"ID": "DestinationRate_AU", "TPid": "cgrates.org", "DestinationRates": \
        [ {"DestinationId": "Dest_AU_Fixed", "RateId": "Rate_AU_Fixed_Rate_1", "Rate": None, "RoundingMethod": "*up", "RoundingDecimals": 4, "MaxCost": 0, "MaxCostStrategy": ""} ]\
    }]})

TPDestinationRates = CGRateS_Obj.SendData({"jsonrpc":"2.0","method":"ApierV1.GetTPDestinationRateIds","params":[{"TPid":"cgrates.org"}]})['result']
for TPDestinationRate in TPDestinationRates:
    pprint.pprint(TPDestinationRate)

All going well, you’ll see the new DestinationRate we added.

Here’s a good chance to show how we can add multiple bits of data in one API call, we can tweak the ApierV1.SetTPDestinationRate method and include all the DestinationRates we need in one API call:

CGRateS_Obj.SendData({"method": "ApierV1.SetTPDestinationRate", "params": [
        {"ID": "DestinationRate_AU", "TPid": "cgrates.org", "DestinationRates": [ \
            {"DestinationId": "Dest_AU_Fixed", "RateId": "Rate_AU_Fixed_Rate_1", "Rate": None, "RoundingMethod": "*up", "RoundingDecimals": 4, "MaxCost": 0, "MaxCostStrategy": ""},\
            {"DestinationId": "Dest_AU_Mobile", "RateId": "Rate_AU_Mobile_Rate_1", "Rate": None, "RoundingMethod": "*up", "RoundingDecimals": 4, "MaxCost": 0, "MaxCostStrategy": ""}, \
            {"DestinationId": "Dest_AU_TollFree", "RateId": "Rate_AU_Toll_Free_Rate_1", "Rate": None, "RoundingMethod": "*up", "RoundingDecimals": 4, "MaxCost": 0, "MaxCostStrategy": ""}\
     ]},
    ]})

As we’ve only created one DestinationRate, let’s take a look at the detail:

TPDestinationRate = CGRateS_Obj.SendData({"jsonrpc":"2.0","method":"ApierV1.GetTPDestinationRate","params":[{"ID":"DestinationRate_AU","TPid":"cgrates.org"}],"id":1})
pprint.pprint(TPDestinationRate)

Phew, okay, if you made it this far, congratulations.

So where we stand now is we’ve created Rates, Destinations and tied the two together.

I’ve put a copy of all the Python code on GitHub here, in case you’re having issues you can work with that.

In our next post, we’ll keep working our way up this diagram, by creating RatingPlans and RatingProfiles to reference the DestinationRate we just created.

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.

Kamailio I-CSCF – SRV Lookup Behaviour

Recently I had a strange issue I thought I’d share.

Using Kamailio as an Interrogating-CSCF, Kamailio was getting the S-CSCF details from the User-Authorization-Answer’s “Server-Name” (602) AVP.

The value was set to:

sip:scscf.mnc001.mcc001.3gppnetwork.org:5060

But the I-CSCF was only looking up A-Records for scscf.mnc001.mcc001.3gppnetwork.org, not using DNS-SRV.

The problem? The Server-Name I had configured as a full SIP URI in PyHSS including the port, meant that Kamailio only looks up the A-Record, and did not do a DNS-SRV lookup for the domain.

Dropping the port number saw all those delicious SRV records being queried.

Something to keep in mind if you use S-CSCF pooling with a Kamailio based I-CSCF, if you want to use SRV records for load balancing / traffic sharing, don’t include the port, and if instead you want it to go to the specified host found by an A-record, include the port.

Kamailio Bytes – Extracting SDP Parameters with Kamailio

So the other day I needed to extract the IP and Port parameters from an SDP body – Not the whole line mind, but the values themselves.

As with so many things in Kamailio, there’s a lot of ways to achieve an outcome, but here’s how I approached this problem.

Using the SDPops module we can get a particular line in the SDP, for example, we can get the media line with:

#Get SDP line starting with m= and put it into AVP $avp(mline)
sdp_get_line_startswith("$avp(mline)", "m=")
#Print value of $avp(mline)
xlog("m-line: $avp(mline)\n");

This gets us the line, but now we need to extract the data, in the example from the screenshot the M line has the value:

m=audio 4002 RTP/AVP 8 101

But we only want the port from the M line.

This is where I’ve used the Kamailio Dialplan module and regex to extract the port from this line.

With a fairly simple regex pattern, we can get a group match for the Port from the m= line.

So I took this regular expression, and put it into the Kamailio Dialplan database with dialplan ID 400 for this example:

INSERT INTO `dialplan` VALUES (4,400,10,1,'m=audio (\\d*)',0,'m=audio (\\d*)','\\1','SDP M Port Stripper');

Now using Dialplan ID 400 we can translate an inputted m= SDP line, and get back the port used, so let’s put that into practice:

        if(sdp_get_line_startswith("$avp(mline)", "m=")) {
            xlog("m-line: $avp(mline)\n");
            xlog("raw: $avp(mline)");
            xlog("Extracting Port from Media Line");
            dp_translate("400", "$avp(mline)/$avp(m_port_b_leg)");
            xlog("Translated m_port_b_leg is: $avp(m_port_b_leg)");
        }

Now we have an AVP called $avp(m_port_b_leg) which contains the RTP Port from the SDP.

Now we’ve got a few other values we might want to get, such as the IP the RTP is to go to, etc, we can extract this in the same way, with Dialplans and store them as AVPs:

        #Print current SDP Values and store as Vars
        if(sdp_get_line_startswith("$avp(mline)", "m=")) {
            xlog("m-line: $avp(mline)\n");
            xlog("raw: $avp(mline)");
            xlog("Extracting Port from Media Line");
            dp_translate("400", "$avp(mline)/$avp(m_port_b_leg)");
            xlog("Translated m_port_b_leg is: $avp(m_port_b_leg)");
        }

        if(sdp_get_line_startswith("$avp(oline)", "o=")) {
            xlog("o-line: $avp(oline)\n");
            dp_translate("401", "$avp(oline)/$avp(o_line_port_1)");
            xlog("O Line Port 1: $avp(o_line_port_1)");
            dp_translate("402", "$avp(oline)/$avp(o_line_port_2)");
            xlog("O Line Port 2: $avp(o_line_port_2)");
            dp_translate("403", "$avp(oline)/$avp(o_ip_b_leg)");
            xlog("O IP: $avp(o_ip_b_leg)");
        }

And all the Regex you’ll need:

INSERT INTO `dialplan` VALUES 
(4,400,10,1,'m=audio (\\d*)',0,'m=audio (\\d*)','\\1','SDP M Port Stripper'),
(5,401,10,1,'o=[^ ]* (\\d*) (\\d*) IN IP4 (\\d*.d*.\\d*.\\d*)',0,'o=[^ ]* (\\d*) (\\d*) IN IP4 (\\d*.d*.\\d*.\\d*)','\\1','O Port 1'),
(6,402,10,1,'o=[^ ]* (\\d*) (\\d*) IN IP4 (\\d*.d*.\\d*.\\d*)',0,'o=[^ ]* (\\d*) (\\d*) IN IP4 (\\d*.d*.\\d*.\\d*)','\\2','O Port 2'),
(7,403,10,1,'o=[^ ]* (\\d*) (\\d*) IN IP4 (\\d*.d*.\\d*.\\d*)',0,'o=[^ ]* (\\d*) (\\d*) IN IP4 (\\d*[.]\\d*[.]\\d*[.]\\d*)','\\3','O IP');


SMS with Alphanumeric Source

Sending SMS with an alphanumeric String as the Source

If you’ve ever received an SMS from your operator, and the sender was the Operator name for example, you may be left wondering how it’s done.

In IMS you’d think this could be quite simple – You’d set the From header to be the name rather than the MSISDN, but for most SMSoIP deployments, the From header is ignored and instead the c header inside the SMS body is used.

So how do we get it to show text?

Well the TP-Originating address has the “Type of Number” (ToN) field which is typically set to International/National, but value 5 allows for the Digits to instead be alphanumeric characters.

GSM 7 bit encoding on the text in the TP-Originating Address digits and presto, you can send SMS to subscribers where the message shows as From an alphanumeric source.

On Android SMSs received from alphanumeric sources cannot be responded to (“no more “DO NOT REPLY TO THIS MESSAGE” at the end of each text), but on iOS devices you can respond, but if I send an SMS from “Nick” the reply from the subscriber using the iPhone will be sent to MSISDN 6425 (Nick on the telephone keypad).

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
IMS DNS Failing

Kamailio, IMS & DNS Headches

I’m sure I’ve ranted about the importance of DNS in IMS networks in the past on here already.

Recently I was rebuilding a P-CSCF and kept getting an error saying that the DNS was failing to resolve:

 4(5993) CRITICAL: <core> [core/dns_cache.c:3136]: dns_srv_sip_resolve(): unknown proto 0
 4(5993) ERROR: tm [ut.h:284]: uri2dst2(): failed to resolve "ims.mnc001.mcc001.3gppnetwork.org" :bug - critical error (-13)
 4(5993) ERROR: tm [t_fwd.c:1759]: t_forward_nonack(): failure to add branches
 4(5993) ERROR: sl [sl_funcs.c:414]: sl_reply_error(): stateless error reply used: Unresolvable destination (478/SL)

This was a rebuild, another P-CSCF was running fine and handling traffic with the same DNS server set.

I checked the netplan config and confirmed the DNS server was set correctly.

If I did an nslookup on the address that was failing to resolve – pointing it at the correct DNS server, the A & SRV records came back OK, and everything was fine.

Stranger still, after clearing the DNS Cache, and running a packet capture, I couldn’t see any DNS queries at all….

The problem? Kamailio uses resolv.conf by default on Ubuntu Server, and that was pointing to localhost.

After updating resolv.conf to point to the DNS server handling the IMS domains, I was good to go again.

A super valuable resource for all things DNS & Kamailio is this doc.

FreeSWITCH, Kamailio & IMS Extensions

Recently I’ve been doing some work with FreeSWITCH as an IMS Conference Factory, I’ve written a bit about it before in this post on using FreeSWITCH with the AMR codec.

Pretty early on in my testing I faced a problem with subsequent in-dialog responses, like re-INVITEs used for holding the calls.

Every subsequent message, was getting a “420 Bad Extension” response from FreeSWITCH.

So what didn’t it like and why was FreeSWITCH generating 420 Bad Extension Responses to these subsequent messages?

Well, the “Extensions” FreeSWITCH is referring to are not extensions in the Telephony sense – as in related to the Dialplan, like an Extension Number to identify a user, but rather the Extensions (as in expansions) to the SIP Protocol introduced for IMS.

The re-INVITE contains a Require header with sec-agree which is a SIP Extension introduced for IMS, which FreeSWITCH does not have support for, and the re-INVITE says is required to support the call (Not true in this case).

Using a Kamailio based S-CSCF means it is easy to strip these Headers before forwarding the requests onto the Application Server, which is what I’ve done, and bingo, no more errors!

CGrates in Baby Steps – Part 1

So you have a VoIP service and you want to rate the calls to charge your customers?

You’re running a mobile network and you need to meter data used by subscribers?

Need to do least-cost routing?

You want to offer prepaid mobile services?

Want to integrate with Asterisk, Kamailio, FreeSWITCH, Radius, Diameter, Packet Core, IMS, you name it!

Well friends, step right up, because today, we’re talking CGrates!

So before we get started, this isn’t going to be a 5 minute tutorial, I’ve a feeling this may end up a big multipart series like some of the others I’ve done.
There is a learning curve here, and we’ll climb it together – but it is a climb.

Installation

Let’s start with a Debian based OS, installation is a doddle:

sudo wget -O - https://apt.cgrates.org/apt.cgrates.org.gpg.key | sudo apt-key add -
echo "deb http://apt.cgrates.org/debian/ nightly main" | sudo tee /etc/apt/sources.list.d/cgrates.list
sudo apt-get update
sudo apt-get install cgrates -y
apt-get install mysql-server redis-server git -y

We’re going to use Redis for the DataDB and MariaDB as the StorDB (More on these concepts later), you should know that other backend options are available, but for keeping things simple we’ll just use these two.

Next we’ll get the database and config setup,

cd /usr/share/cgrates/storage/mysql/
./setup_cgr_db.sh root CGRateS.org localhost
cgr-migrator -exec=*set_versions -stordb_passwd=CGRateS.org

Lastly we’ll clone the config files from the GitHub repo:

https://github.com/nickvsnetworking/CGrates_Tutorial

Rating Concepts

So let’s talk rating.

In its simplest form, rating is taking a service being provided and calculating the cost for it.

The start of this series will focus on voice calls (With SMS, MMS, Data to come), where the calling party (The person making the call) pays, so let’s imagine calling a Mobile number (Starting with 614) costs $0.22 per minute.

To perform rating we need to determine the Destination, the Rate to be applied, and the time to charge for.

For our example earlier, a call to a mobile (Any number starting with 614) should be charged at $0.22 per minute. So a 1 minute call will cost $0.22 and a 2 minute long call will cost $0.44, and so on.

We’ll also charge calls to fixed numbers (Prefix 612, 613, 617 and 617) at a flat $0.20 regardless of how long the call goes for.

So let’s start putting this whole thing together.

Introduction to RALs

RALs is the component in CGrates that takes care of Rating and Accounting Logic, and in this post, we’ll be looking at Rating.

The rates have hierarchical structure, which we’ll go into throughout this post. I took my notepad doodle of how everything fits together and digitized it below:

Destinations

Destinations are fairly simple, we’ll set them up in our Destinations.csv file, and it will look something like this:

#Id,Prefix
DST_AUS_Mobile,614
DST_AUS_Fixed,612
DST_AUS_Fixed,613
DST_AUS_Fixed,617
DST_AUS_Fixed,618
DST_AUS_Toll_Free,611300
DST_AUS_Toll_Free,611800

Each entry has an ID (referred to higher up as the Destination ID), and a prefix.

Also notice that some Prefixes share an ID, for example 612, 613, 617 & 618 are under the Destination ID named “DST_AUS_Fixed”, so a call to any of those prefixes would match DST_AUS_Fixed.

Rates

Rates define the price we charge for a service and are defined by our Rates.csv file.

#Id,ConnectFee,Rate,RateUnit,RateIncrement,GroupIntervalStart
RT_22c_PM,0,22,60s,60s,0s
RT_20c_Untimed,20,0,60s,60s,0s
RT_25c_Flat,25,0,60s,60s,0s

Let’s look at the fields we have:

  • ID (Rate ID)
  • ConnectFee – This is the amount charged when the call is answered / connected
  • The Rate is how much we will charge, it’s loosely cents, but could be any currency. By default CGrates looks down to 4 decimal places.
  • RateUnit is how often this rate is applied in seconds
  • RateIncriment is how often this is evaluated in seconds
  • GroupIntervalStart – Activates an event when triggered

So let’s look at how this could be done, and the gotchas that exist.

So let’s look at some different use cases and how we’d handle them.

Per Minute Billing

This would charge a rate per minute, at the start of the call, the first 60 seconds will cost the caller $0.25.

At the 61 second mark, they will be charged another $0.25.

60 seconds after that they will be charged another $0.25 and so on.

#Id,ConnectFee,Rate,RateUnit,RateIncrement,GroupIntervalStart
RT_25c_PM_PerMinute_Billing,0,25,60s,60s,0s

This is nice and clean, a 1 second call costs $0.25, a 60 second call costs $0.25, and a 61 second call costs $0.50, and so on.

This is the standard billing mechanism for residential services, but it does not pro-rata the call – For example a 1 second call is the same cost as a 59 second call ($0.25), and only if you tick over to 61 seconds does it get charged again (Total of $0.50).

Per Second Billing

If you’re doing a high volume of calls, paying for a 3 second long call where someone’s voicemail answers the call and was hung up, may seem a bit steep to pay the same for that as you would pay for 59 seconds of talk time.

Instead Per Second Billing is more common for high volume customers or carrier-interconnects.

This means the rate still be set at $0.25 per minute, but calculated per second.

So the cost of 60 seconds of call is $0.25, but the cost of 30 second call (half a minute) should cost half of that, so a 30 second call would cost $0.125.

#Id,ConnectFee,Rate,RateUnit,RateIncrement,GroupIntervalStart
RT_25c_PM_PerSecond_Billing,0,25,60s,1s,0s

How often we asses the charging is defined by the RateIncrement parameter in the Rate Table.

We could achieve the same outcome another way, by setting the RateIncriment to 1 second, and the dividing the rate per minute by 60, we would get the same outcome, but would be more messy and harder to maintain, but you could think of this as $0.25 per minute, or $0.004166667 per second ($0.25/60 seconds).

Flat Rate Billing

Another option that’s commonly used is to charge a flat rate for the call, so when the call is answered, you’re charged that rate, regardless of the length of the call.

Regardless if the call is for 1 second or 10 hours, the charge is the same.

#Id,ConnectFee,Rate,RateUnit,RateIncrement,GroupIntervalStart
RT_25c_Flat,25,0,60s,60s,0s

For this we just set the ConnectFee, leaving the Rate at 0, so the cost will be applied on connection, with no costs applied per time period.

This means a 1 second call will cost $0.25, while a 3600 second call will still cost $0.25.

We charge a connect fee, but no rate.

Linking Destinations to the Rates to Charge

Now we’ve defined our Destinations and our Rates, we can link the two, defining what Destinations get charged what Rates.

This is defined in DestinationRates.csv

#Id,DestinationId,RatesTag,RoundingMethod,RoundingDecimals,MaxCost,MaxCostStrategy
DR_AUS_Mobile,DST_AUS_Mobile,RT_22c_PM,*up,4,0.12,*disconnect
DR_AUS_Fixed,DST_AUS_Fixed,RT_20c_Untimed,*up,4,0.12,*disconnect
DR_AUS_Toll_Free,DST_AUS_Toll_Free,RT_25c_Flat,*up,4,0.12,*disconnect

Let’s look at the Fields,

  • ID (Destination Rate ID)
  • DestinationID – Refers to the DestinationID defined in the Destinations.csv file
  • RatesTag – Referes to the Rate ID we defined in Rates.csv
  • RoundingMethod – Defines if we round up or down
  • RoundingDecimals – Defines how many decimal places to consider before rounding
  • MaxCost – The maximum cost this can go up to
  • MaxCostStrategy – What to do if the Maximum Cost is reached – Either make the rest of the call Free or Disconnect the call

So for each entry we’ll define an ID, reference the Destination and the Rate to be applied, the other parts we’ll leave as boilerplate for now, and presto. We have linked our Destinations to Rates.

Rating Plans

We may want to offer different plans for different customers, with different rates.

That’s what we define in our Rating Plans.

#Id,DestinationRatesId,TimingTag,Weight
RP_AUS,DR_AUS_Mobile,*any,10
RP_AUS,DR_AUS_Fixed,*any,10
RP_AUS,DR_AUS_Toll_Free,*any,10
  • ID (RatingPlanID)
  • DestinationRatesId (As defined in DestinationRates.csv)
  • TimingTag – References a time profile if used
  • Weight – Used to determine what precedence to use if multiple matches

So as you may imagine we need to link the DestinationRateIDs we just defined together into a Rating Plan, so that’s what I’ve done in the example above.

Rating Profiles

The last step in our chain is to link Customers / Subscribers to the profiles we’ve just defined.

How you allocate a customer to a particular Rating Plan is up to you, there’s numerous ways to approach it, but for this example we’re going to use one Rating Profile for all callers coming from the “cgrates.org” tenant:

#Tenant,Category,Subject,ActivationTime,RatingPlanId,RatesFallbackSubject
cgrates.org,call,*any,2014-01-14T00:00:00Z,RP_AUS,

Let’s go through the fields here,

  • Tenant is a grouping of Customers
  • Category is used to define the type of service we’re charging for, in this case it’s a call, but could also be an SMS, Data usage, or a custom definition.
  • Subject is typically the calling party, we could set this to be the Caller ID, but in this case I’ve used a wildcard “*any”
  • ActivationTime allows us to define a start time for the Rating Profile, for example if all our rates go up on the 1st of each month, we can update the Plans and add a new entry in the Rating Profile with the new Plans with the start time set
  • RatingPlanID sets the Rating Plan that is used as we defined in RatingPlans.csv

Loading the Rates into CGrates

At the start we’ll be dealing with CGrates through CSV files we import, this is just one way to interface with CGrates, there’s others we’ll cover in due time.

CGRates has a clever realtime architecture that we won’t go into in any great depth, but in order to load data in from a CSV file there’s a simple handy tool to run the process,

root@cgrateswitch:/home/nick# cgr-loader -verbose -path=/home/nick/tutorial/ -flush_stordb

Obviously you’ll need to replace with the folder you cloned from GitHub.

Trying it Out

In order for CGrates to work with Kamailio, FreeSWITCH, Asterisk, Diameter, Radius, and a stack of custom options, for rating calls, it has to have common mechanisms for retrieving this data.

CGrates provides an API for rating calls, that’s used by these platforms, and there’s a tool we can use to emulate the signaling for call being charged, without needing to pickup the phone or integrate a platform into it.

root@cgrateswitch:/home/nick# cgr-console 'cost Category="call" Tenant="cgrates.org" Subject="3005" Destination="614" AnswerTime="2014-08-04T13:00:00Z" Usage="60s"'

The tenant will need to match those defined in the RatingProfiles.csv, the Subject is the Calling Party identity, in our case we’re using a wildcard match so it doesn’t matter really what it’s set to, the Destination is the destination of the call, AnswerTime is time of the call being answered (pretty self explanatory) and the usage defines how many seconds the call has progressed for.

The output is a JSON string, containing a stack of useful information for us, including the Cost of the call, but also the rates that go into the decision making process so we can see the logic that went into the price.

{
 "AccountSummary": null,
 "Accounting": {},
 "CGRID": "",
 "Charges": [
  {
   "CompressFactor": 1,
   "Increments": [
    {
     "AccountingID": "",
     "CompressFactor": 1,
     "Cost": 0,
     "Usage": "0s"
    },
    {
     "AccountingID": "",
     "CompressFactor": 1,
     "Cost": 25,
     "Usage": "1m0s"
    }
   ],
   "RatingID": "febb614"
  }
 ],
 "Cost": 25,
 "Rates": {
  "7d4a755": [
   {
    "GroupIntervalStart": "0s",
    "RateIncrement": "1m0s",
    "RateUnit": "1m0s",
    "Value": 25
   }
  ]
 },
 "Rating": {
  "febb614": {
   "ConnectFee": 0,
   "MaxCost": 0.12,
   "MaxCostStrategy": "*disconnect",
   "RatesID": "7d4a755",
   "RatingFiltersID": "7e42edc",
   "RoundingDecimals": 4,
   "RoundingMethod": "*up",
   "TimingID": "c15a254"
  }
 },
 "RatingFilters": {
  "7e42edc": {
   "DestinationID": "DST_AUS_Mobile",
   "DestinationPrefix": "614",
   "RatingPlanID": "RP_AUS",
   "Subject": "*out:cgrates.org:call:3005"
  }
 },
 "RunID": "",
 "StartTime": "2014-08-04T13:00:00Z",
 "Timings": {
  "c15a254": {
   "MonthDays": [],
   "Months": [],
   "StartTime": "00:00:00",
   "WeekDays": [],
   "Years": []
  }
 },
 "Usage": "1m0s"
}

So have a play with setting up more Destinations, Rates, DestinationRates and RatingPlans, in these CSV files, and in our next post we’ll dig a little deeper… And throw away the CSVs all together!