Tag Archives: Diameter

Diameter Routing Agents – Part 4 – Advanced FreeDiameter DRA Routing

Way back in part 2 we discussed the basic routing logic a DRA handles, but what if we want to do something a bit outside of the box in terms of how we route?

For me, one of the most useful use cases for a DRA is to route traffic based on IMSI / Username.
This means I can route all the traffic for MVNO X to MVNO X’s HSS, or for staging / test subs to the test HSS enviroment.

FreeDiameter has a bunch of built in logic that handles routing based on a weight, but we can override this, using the rt_default module.

In our last post we had this module commented out, but let’s uncomment it and start playing with it:

#Basic Diameter config for this box
Identity = "dra.mnc001.mcc001.3gppnetwork.org";
Realm = "mnc001.mcc001.3gppnetwork.org";
Port = 3868;

LoadExtension = "dbg_msg_dumps.fdx" : "0x8888";
LoadExtension = "rt_redirect.fdx":"0x0080";
LoadExtension = "rt_default.fdx":"rt_default.conf";

TLS_Cred = "/etc/freeDiameter/cert.pem", "/etc/freeDiameter/privkey.pem";
TLS_CA = "/etc/freeDiameter/cert.pem";
TLS_DH_File = "/etc/freeDiameter/dh.pem";

ConnectPeer = "mme01.mnc001.mcc001.3gppnetwork.org" { ConnectTo = "10.98.0.10"; No_TLS; };
ConnectPeer = "hss01" { ConnectTo = "10.0.1.252"; No_TLS; Port = 3868; Realm = "mnc001.mcc001.3gppnetwork.org";};
ConnectPeer = "hss02" { ConnectTo = "10.0.1.253"; No_TLS; Port = 3868; Realm = "mnc001.mcc001.3gppnetwork.org";};
ConnectPeer = "hss-mvno-x" { ConnectTo = "10.98.0.22"; No_TLS; Port = 3868; Realm = "mnc001.mcc001.3gppnetwork.org";};
ConnectPeer = "hss-lab" { ConnectTo = "10.0.2.2"; No_TLS; Port = 3868; Realm = "mnc001.mcc001.3gppnetwork.org";};

In the above code we’ve uncommented rt_default and rt_redirect.

You’ll notice that rt_default references a config file, so we’ll create a new file in our /etc/freeDiameter directory called rt_default.conf, and this is where the magic will happen.

A few points before we get started:

  • This overrides the default routing priorities, but in order for a peer to be selected, it has to be in an Open (active) state
  • The peer still has to have advertised support for the requested application in the CER/CEA dialog
  • The peers will still need to have all been defined in the freeDiameter.conf file in order to be selected

So with that in mind, and the 5 peers we have defined in our config above (assuming all are connected), let’s look at some rules we can setup using rt_default.

Intro to rt_default Rules

The rt_default.conf file contains a list of rules, each rule has a criteria that if matched, will result in the specified action being taken. The actions all revolve around how to route the traffic.

So what can these criteria match on?
Here’s the options:

Item to MatchCode
Any*
Origin-Hostoh=”STR/REG”
Origin-Realmor=”STR/REG”
Destination-Hostdh=”STR/REG”
Destination-Realmdr=”STR/REG”
User-Nameun=”STR/REG”
Session-Idsi=”STR/REG”
rt_default Matching Criteria

We can either match based on a string or a regex, for example, if we want to match anything where the Destination-Realm is “mnc001.mcc001.3gppnetwork.org” we’d use something like:

#Low score to HSS02
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss02" += -70 ;

Now you’ll notice there is some stuff after this, let’s look at that.

We’re matching anything where the destination-host is set to hss02 (that’s the bit before the colon), but what’s the bit after that?

Well if we imagine that all our Diameter peers are up, when a message comes in with Destination-Realm “mnc001.mcc001.3gppnetwork.org”, looking for an HSS, then in our example setup, we have 4 HHS instances to choose from (assuming they’re all online).

In default Diameter routing, all of these peers are in the same realm, and as they’re all HSS instances, they all support the same applications – Our request could go to any of them.

But what we set in the above example is simply the following:

If the Destination-Realm is set to mnc001.mcc001.3gppnetwork.org, then set the priority for routing to hss02 to the lowest possible value.

So that leaves the 3 other Diameter peers with a higher score than HSS02, so HSS02 won’t be used.

Let’s steer this a little more,

Let’s specify that we want to use HSS01 to handle all the requests (if it’s available), we can do that by adding a rule like this:

#Low score to HSS02
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss02" += -70 ;
#High score to HSS01
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss01" += 100 ;

But what if we want to route to hss-lab if the IMSI matches a specific value, well we can do that too.

#Low score to HSS02
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss02" += -70 ;
#High score to HSS01
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss01" += 100 ;
#Route traffic for IMSI to Lab HSS
un="001019999999999999" : dh="hss-lab" += 200 ;

Now that we’ve set an entry with a higher score than hss01 that will be matched if the username (IMSI) equals 001019999999999999, the traffic will get routed to hss-lab.

But that’s a whole IMSI, what if we want to match only part of a field?

Well, we can use regex in the Criteria as well, so let’s look at using some Regex, let’s say for example all our MVNO SIMs start with 001012xxxxxxx, let’s setup a rule to match that, and route to the MVNO HSS with a higher priority than our normal HSS:

#Low score to HSS02
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss02" += -70 ;
#High score to HSS01
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss01" += 100 ;
#Route traffic for IMSI to Lab HSS
un="001019999999999999" : dh="hss-lab" += 200 ;
#Route traffic where IMSI starts with 001012 to MVNO HSS
un=["^001012.*"] : dh="hss-mvno-x" += 200 ;

Let’s imagine that down the line we introduce HSS03 and HSS04, and we only want to use HSS01 if HSS03 and HSS04 are unavailable, and only to use HSS02 no other HSSes are available, and we want to split the traffic 50/50 across HSS03 and HSS04.

Firstly we’d need to add HSS03 and HSS04 to our FreeDiameter.conf file:

...
ConnectPeer = "hss02" { ConnectTo = "10.0.1.253"; No_TLS; Port = 3868; Realm = "mnc001.mcc001.3gppnetwork.org";};
ConnectPeer = "hss03" { ConnectTo = "10.0.3.3"; No_TLS; Port = 3868; Realm = "mnc001.mcc001.3gppnetwork.org";};
ConnectPeer = "hss04" { ConnectTo = "10.0.4.4"; No_TLS; Port = 3868; Realm = "mnc001.mcc001.3gppnetwork.org";};
...

Then in our rt_default.conf we’d need to tweak our scores again:

#Low score to HSS02
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss02" += 10 ;
#Medium score to HSS01
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss01" += 20 ;
#Route traffic for IMSI to Lab HSS
un="001019999999999999" : dh="hss-lab" += 200 ;
#Route traffic where IMSI starts with 001012 to MVNO HSS
un=["^001012.*"] : dh="hss-mvno-x" += 200 ;
#High Score for HSS03 and HSS04
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss02" += 100 ;
dr="mnc001.mcc001.3gppnetwork.org" : dh="hss04" += 100 ;

One quick tip to keep your logic a bit simpler, is that we can set a variety of different values based on keywords (listed below) rather than on a weight/score:

BehaviourNameScore
Do not deliver to peer (set lowest priority)NO_DELIVERY-70
The peer is a default route for all messagesDEFAULT5
The peer is a default route for this realmDEFAULT_REALM10
REALM15
Route to the specified Host with highest priorityFINALDEST100
Rather than manually specifying the store you can use keywords like above to set the value

In our next post we’ll look at using FreeDiameter based DRA in roaming scenarios where we route messages across Diameter Realms.

Diameter Routing Agents – Part 3 – Building a DRA with FreeDiameter

I’ve covered the basics of Diameter Routing Agents (DRAs) in the past, and even shared an unstable DRA built using Kamailio, but today I thought I’d cover building something a little more “production ready”.

FreeDiameter has been around for a while, and we’ve covered configuring the FreeDiameter components in Open5GS when it comes to the S6a interface, so you may have already come across FreeDiameter in the past, but been left a bit baffled as to how to get it to actually do something.

FreeDiameter is a FOSS implimentation of the Diameter protocol stack, and is predominantly used as a building point for developers to build Diameter applications on top of.

But for our scenario, we’ll just be using plain FreeDiameter.

So let’s get into it,

You’ll need FreeDiameter installed, and you’ll need a certificate for your FreeDiameter instance, more on that in this post.

Once that’s setup we’ll need to define some basics,

Inside freeDiameter.conf we’ll need to include the identity of our DRA, load the extensions and reference the certificate files:

#Basic Diameter config for this box
Identity = "dra.mnc001.mcc001.3gppnetwork.org";
Realm = "mnc001.mcc001.3gppnetwork.org";
Port = 3868;

LoadExtension = "dbg_msg_dumps.fdx" : "0x8888";
#LoadExtension = "rt_redirect.fdx":"0x0080";
#LoadExtension = "rt_default.fdx":"rt_default.conf";

TLS_Cred = "/etc/freeDiameter/cert.pem", "/etc/freeDiameter/privkey.pem";
TLS_CA = "/etc/freeDiameter/cert.pem";
TLS_DH_File = "/etc/freeDiameter/dh.pem";

Next up we’ll need to define the Diameter peers we’ll be routing between.

For each connection / peer / host we’ll need to define here:

ConnectPeer = "mme01.mnc001.mcc001.3gppnetwork.org" { ConnectTo = "10.98.0.10"; No_TLS; };
ConnectPeer = "hss01" { ConnectTo = "10.0.1.252"; No_TLS; Port = 3868; Realm = "mnc001.mcc001.3gppnetwork.org";};

And we’ll configure our HSS and MME defined in the ConnectPeers to connect/accept connections from, dra.mnc001.mcc001.3gppnetwork.org.

Now if we start freeDiameter, we can start routing between the hosts. No config needed.

If we define another HSS in the ConnectPeers, any S6a requests from the MME may get routed to that as well (50/50 split).

In our next post, we’ll look at using the rt_default extension to control how we route and look at some more advanced use cases.

Diameter Routing Agents (Why you need them, and how to build them) – Part 2 – Routing

What I typically refer to as Diameter interfaces / reference points, such as S6a, Sh, Sx, Sy, Gx, Gy, Zh, etc, etc, are also known as Applications.

Diameter Application Support

If you look inside the Capabilities Exchange Request / Answer dialog, what you’ll see is each side advertising the Applications (interfaces) that they support, each one being identified by an Application ID.

CER showing support for the 3GPP Zh Application-ID (Interface)

If two peers share a common Application-Id, then they can communicate using that Application / Interface.

For example, the above screenshot shows a peer with support for the Zh Interface (Spoiler alert, XCAP Gateway / BSF coming soon!). If two Diameter peers both have support for the Zh interface, then they can use that to send requests / responses to each other.

This is the basis of Diameter Routing.

Diameter Routing Tables

Like any router, our DRA needs to have logic to select which peer to route each message to.

For each Diameter connection to our DRA, it will build up a Diameter Routing table, with information on each peer, including the realm and applications it advertises support for.

Then, based on the logic defined in the DRA to select which Diameter peer to route each request to.

In its simplest form, Diameter routing is based on a few things:

  1. Look at the DestinationRealm, and see if we have any peers at that realm
  2. If we do then look at the DestinationHost, if that’s set, and the host is connected, and if it supports the specified Application-Id, then route it to that host
  3. If no DestinationHost is specified, look at the peers we have available and find the one that supports the specified Application-Id, then route it to that host
Simplified Diameter Routing Table used by DRAs

With this in mind, we can go back to looking at how our DRA may route a request from a connected MME towards an HSS.

Let’s look at some examples of this at play.

The request from MME02 is for DestinationRealm mnc001.mcc001.3gppnetwork.org, which our DRA knows it has 4 connected peers in (3 if we exclude the source of the request, as we don’t want to route it back to itself of course).

So we have 3 contenders still for who could get the request, but wait! We have a DestinationHost specified, so the DRA confirms the host is available, and that it supports the requested ApplicationId and routes it to HSS02.

So just because we are going through a DRA does not mean we can’t specific which destination host we need, just like we would if we had a direct link between each Diameter peer.

Conversely, if we sent another S6a request from MME01 but with no DestinationHost set, let’s see how that would look.

Again, the request is from MME02 is for DestinationRealm mnc001.mcc001.3gppnetwork.org, which our DRA knows it has 3 other peers it could route this to. But only two of those peers support the S6a Application, so the request would be split between the two peers evenly.

Clever Routing with DRAs

So with our DRA in place we can simplify the network, we don’t need to build peer links between every Diameter device to every other, but let’s look at some other ways DRAs can help us.

Load Control

We may want to always send requests to HSS01 and only use HSS02 if HSS01 is not available, we can do this with a DRA.

Or we may want to split load 75% on one HSS and 25% on the other.

Both are great use cases for a DRA.

Routing based on Username

We may want to route requests in the DRA based on other factors, such as the IMSI.

Our IMSIs may start with 001010001xxx, but if we introduced an MVNO with IMSIs starting with 001010002xxx, we’d need to know to route all traffic where the IMSI belongs to the home network to the home network HSS, and all the MVNO IMSI traffic to the MVNO’s HSS, and DRAs handle this.

Inter-Realm Routing

One of the main use cases you’ll see for DRAs is in Roaming scenarios.

For example, if we have a roaming agreement with a subscriber who’s IMSIs start with 90170, we can route all the traffic for their subs towards their HSS.

But wait, their Realm will be mnc901.mcc070.3gppnetwork.org, so in that scenario we’ll need to add a rule to route the request to a different realm.

DRAs handle this also.

In our next post we’ll start actually setting up a DRA with a default route table, and then look at some more advanced options for Diameter routing like we’ve just discussed.

One slight caveat, is that mutual support does not always mean what you may expect.
For example an MME and an HSS both support S6a, which is identified by Auth-Application-Id 16777251 (Vendor ID 10415), but one is a client and one is a server.
Keep this in mind!

Diameter Routing Agents (Why you need them, and how to build them) – Part 1

Answer Question 1: Because they make things simpler and more flexible for your Diameter traffic.
Answer Question 2: With free software of course!

All about DRAs

But let’s dive a little deeper. Let’s look at the connection between an MME and an HSS (the S6a interface).

Direct Diameter link between two Diameter Peers

We configure the Diameter peers on MME1 and HSS01 so they know about each other and how to communicate, the link comes up and presto, away we go.

But we’re building networks here! N+1 redundancy and all that, so now we have two HSSes and two MMEs.

Direct Diameter link between 4 Diameter peers

Okay, bit messy, but that’s okay…

But then our network grows to 10 MMEs, and 3 HSSes and you can probably see where this is going, but let’s drive the point home.

Direct Diameter connections for a network with 10x MME and 3x HSS

Now imagine once you’ve set all this up you need to do some maintenance work on HSS03, so need to shut down the Diameter peer on 10 different MMEs in order to isolate it and deisolate it.

The problem here is pretty evident, all those links are messy, cumbersome and they just don’t scale.

If you’re someone with a bit of networking experience (and let’s face it, you’re here after all), then you’re probably thinking “What if we just had a central system to route all the Diameter messages?”

An Agent that could Route Diameter, a Diameter Routing Agent perhaps…

By introducing a DRA we build Diameter peer links between each of our Diameter devices (MME / HSS, etc) and the DRA, rather than directly between each peer.

Then from the DRA we can route Diameter requests and responses between them.

Let’s go back to our 10x MME and 3x HSS network and see how it looks with a DRA instead.

So much cleaner!

Not only does this look better, but it makes our life operating the network a whole lot easier.

Each MME sends their S6a traffic to the DRA, which finds a healthy HSS from the 3 and sends the requests to it, and relays the responses as well.

We can do clever load balancing now as well.

Plus if a peer goes down, the DRA detects the failure and just routes to one of the others.

If we were to introduce a new HSS, we wouldn’t need to configure anything on the MMEs, just add HSS04 to the DRA and it’ll start getting traffic.

Plus from an operations standpoint, now if we want to to take an HSS offline for maintenance, we just shut down the link on the HSS and all HSS traffic will get routed to the other two HSS instances.

In our next post we’ll talk about the Routing part of the DRA, how the decisions are made and all the nuances, and then in the following post we’ll actually build a DRA and start routing some traffic around!

FreeDiameter – Generating Certificates

Even if you’re not using TLS in your FreeDiameter instance, you’ll still need a certificate in order to start the stack.

Luckily, creating a self-signed certificate is pretty simple,

Firstly we generate your a private key and public certificate for our required domain – in the below example I’m using dra01.epc.mnc001.mcc001.3gppnetwork.org, but you’ll need to replace that with the domain name of your freeDiameter instance.

openssl req -new -batch -x509 -days 3650 -nodes     \
   -newkey rsa:1024 -out /etc/freeDiameter/cert.pem -keyout /etc/freeDiameter/privkey.pem \
   -subj /CN=dra01.epc.mnc001.mcc001.3gppnetwork.org

Next we generate a new set of Diffie-Hellman parameter set using OpenSSL.

openssl dhparam -out /etc/freeDiameter/dh.pem 1024 

Lastly we’ll put all this config into the freeDiameter config file:

TLS_Cred = "/etc/freeDiameter/cert.pem", "/etc/freeDiameter/privkey.pem";
TLS_CA = "/etc/freeDiameter/cert.pem";
TLS_DH_File = "/etc/freeDiameter/dh.pem";

If you’re using freeDiameter as part of another software stack (Such as Open5Gs) the below filenames will contain the config for that particular freeDiameter components of the stack:

  • freeDiameter.conf – Vanilla freeDiameter
  • mme.conf – Open5Gs MME
  • pcrf.conf – Open5Gs PCRF
  • smf.conf – Open5Gs SMF / P-GW-C
  • hss.conf – Open5Gs HSS

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. We can use Category to link what RatingPlan is used, for example we might want to offer a premium voice service with guaranteed CLI rates, using a different RatingPlan that charges more per call, or maybe we’re doing mobile and we want a different RatingPlan for use when Roaming, we can use Category to switch that.
  • 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 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!

Evolved Packet Core – Analysis Challenge

This post is one of a series of packet capture analysis challenges designed to test your ability to understand what is going on in a network from packet captures.
Download the Packet Capture and see how many of the questions you can answer from the attached packet capture.

The answers are at the bottom of this page, along with how we got to the answers.

This challenge focuses on the Evolved Packet Core, specifically the S1 and Diameter interfaces.

Why is the Subscriber failing to attach?

And what is the behavior we should be expecting to see?

What is the Cell ID of this eNodeB?

What is the Tracking Area?

That the subscriber is trying to attach in.

Does the device attaching to the network support VoLTE?

What type of IP is the subscriber requesting for this PDN session?

Is the device requesting an IPv4 address, IPv6 address or both?

What is the Diameter Application ID for S6a?

You should be able to ascertain this from information from the PCAP, without needing to refer to the standards.

What is the Crytpo RES returned by the HSS, and what is the RES returned by the SIM/UE?

Does this mean the subscriber was authenticated successfully?

Answers

Answer: Why is the Subscriber failing to attach?

The Diameter Update Location Request in frame 10 does not get answered by the HSS. After 5 seconds the MME gives up and rejects the connection.

Instead what should have happened is the HSS should have responded to the Update Location Request with an Update Location Answer, as we covered in the attach procedure.

Answer: What is the Cell ID of this eNodeB?

In Uplink messages from the eNodeB the EUTRAN-GCI field contains the Cell-ID of the eNodeB.

In this case the Cell-ID is 1.

Answer: What is the Tracking Area?

The tracking area is 123.

This information is available in the TAI field in the Uplink S1 messages.

Answer: Does the device attaching to the network support VoLTE?

No, the device does not support VoLTE.

There are a few ways we can get to this answer, and VoLTE support in the phone does not mean VoLTE will be enabled, but we can see the Voice Domain preference is set to CS Voice Only, meaning GSM/UMTS for voice calling.

This is common on cheaper handsets that do not support VoLTE.

Answer: What type of IP is the subscriber requesting for this PDN session? (IPv4/IPv6/Both)?

The subscriber is requesting an IPv4 address only.

We can see this in the ESM Message Container for the PDN Connectivity Request, the PDN type is “IPv4”.

Answer: What is the Diameter Application ID for S6a?

Answer: 16777251

This is shown for the Vendor-Specific-Application-Id AVP on an S6a message.

Answer: What is the Crytpo RES returned by the HSS, and what is the RES returned by the SIM/UE?

The RES (Response) and X-RES (Expected Response) Both are “dba298fe58effb09“, they do match, which means this subscriber was authenticated successfully.

You can learn more about what these values do in this post.

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.

Lifecycle of a Dedicated Bearer – From Flow-Description AVP to Traffic Flow Templates

To support Dedicated Bearers we first have to have a way of profiling the traffic, to classify the traffic as being the type we want to provide the Dedicated Bearer for.

The first step involves a request from an Application Function (AF) to the PCRF via the Rx interface.

The most common type of AF would be a P-CSCF. When a VoLTE call gets setup the P-CSCF requests that a dedicated bearer be setup for the IP Address and Ports involved in the VoLTE call, to ensure users get the best possible call quality.

But Application Functions aren’t limited to just VoLTE – You could also embed an Application Function into the server for an online game to enable a dedicated bearer for users playing that game, or a sports streaming app that detects when a user starts streaming sports and creates a dedicated bearer for that user to send the traffic down.

The request to setup a dedicated bearer comes in the form of a Diameter request message from the AF, using the Rx reference point, typically from the P-CSCF to the PCRF in the network in an “AA-Request”.

Of main interest in the AA-Request is the Media Component AVP, that contains all the details needed to identify the traffic flow.

Now our PCRF is in charge of policy, and know which P-GW is serving the required subscriber. So the PCRF takes this information and sends a Gx Re-Auth Request to the PCEF in the P-GW serving the subscriber, with a Charging Rule the PCEF in the P-GW needs to install, to profile and apply QoS to the bearer.

So within the Gx Re-Auth Request is the Charging-Rule Definition, made up of Flow-Description AVP which I’ve written about here, that is used to identify and profile traffic flows and QoS parameters to apply to matching traffic.

Charging Rule Definition’s Flow-Information AVPs showing the information needed to profile the traffic

The QoS Description AVP defines which QoS parameters (QCI / ARP / Guaranteed & Maximum Bandwidth) should be applied to the traffic that matches the rules we just defined.

QoS information AVP
QoS Information AVP showing requested QoS Parameters

The P-GW sends back a Gx Re-Auth Answer, and gets to work actually setting up these bearers.

With the rule installed on the PCEF, it’s time to get this new bearer set up on the UE / eNodeB.

The P-GW sends a GTPv2 “Create Bearer Request” to the S-GW which forwards it onto the MME, to setup / define the Dedicated Bearer to be setup on the eNodeB.

GTPv2 “Create Bearer Request” sent by the P-Gw to the S-GW forwarded from the S-GW to the MME

The MME translates this into an S1 “E-RAB Setup Request” which it sends to the eNodeB to setup,

S1 E-RAB Setup request showing the E-RAB to be setup

Assuming the eNodeB has the resources to setup this bearer, it provides the details to the UE and sets up the bearer, sending confirmation back to the MME in the S1 “E-RAB Setup Response” message, which the MME translates back into GTPv2 for a “Create Bearer Response”

All this effort to keep your VoLTE calls sounding great!

Jaffa Cakes explain the nuances between Centralized vs Decentralized Online Charging in 3GPP Networks

While reading through the 3GPP docs regarding Online Charging, there’s a concept that can be a tad confusing, and that’s the difference between Centralized and Non-Centralized Charging architectures.

The overall purpose of online charging is to answer that deceptively simple question of “does the user have enough credit for this action?”.

In order to answer that question, we need to perform rating and unit determination.

Rating

Rating is just converting connectivity credit units into monetary units.

If you go to the supermarket and they have boxes of Jaffa Cakes at $2.50 each, they have rated a box of Jaffa Cakes at $2.50.

1 Box of Jaffa Cakes rated at $2.50 per box

In a non-snack-cake context, such as 3GPP Online Charging, then we might be talking about data services, for example $1 per GB is a rate for data.
Or for a voice calls a cost per minute to call a destination, such as is $0.20 per minute for a local call.

Rating is just working out the cost per connectivity unit (Data or Minutes) into a monetary cost, based on the tariff to be applied to that subscriber.

Unit Determination

The other key piece of information we need is the unit determination which is the calculation of the number of non-monetary units the OCS will offer prior to starting a service, or during a service.

This is done after rating so we can take the amount of credit available to the subscriber and calculate the number of non-monetary units to be offered.

Converting Hard-Currency into Soft-Snacks

In our rating example we rated a box of Jaffa Cakes at $2.50 per box. If I have $10 I can go to the shops and buy 4x boxes of Jaffa cakes at $2.50 per box. The cashier will perform unit determination and determine that at $2.50 per box and my $10, I can have 4 boxes of Jaffa cakes.

Again, steering away from the metaphor of the hungry author, Unit Determination in a 3GPP context could be determining how many minutes of talk time to be granted.
Question: At $0.20 per minute to a destination, for a subscriber with a current credit of $20, how many minutes of talk time should they be granted?
Answer: 100 minutes ($20 divided by $0.20 per minute is 100 minutes).

Or to put this in a data perspective,
Question: Subscriber has $10 in Credit and data is rated at $1 per GB. How many GB of data should the subscriber be allowed to use?
Answer: 10GB.

Putting this Together

So now we understand rating (working out the conversion of connectivity units into monetary units) and unit determination (determining the number of non-monetary units to be granted for a given resource), let’s look at the the Centralized and Decentralized Online Charging.

Centralized Rating

In Centralized Rating the CTF (Our P-GW or S-CSCF) only talk about non-monetary units.
There’s no talk of money, just of the connectivity units used.

The CTFs don’t know the rating information, they have no idea how much 1GB of data costs to transfer in terms of $$$.

For the CTF in the P-GW/PCEF this means it talks to the OCS in terms of data units (data In/out), not money.

For the CTF in the S-CSCF this means it only ever talks to the OCS in voice units (minutes of talk time), not money.

This means our rates only need to exist in the OCS, not in the CTF in the other network elements. They just talk about units they need.

De-Centralized Rating

In De-Centralized Rating the CTF performs the unit conversion from money into connectivity units.
This means the OCS and CTF talk about Money, with the CTF determining from that amount of money granted, what the subscriber can do with that money.

This means the CTF in the S-CSCF needs to have a rating table for all the destinations to determine the cost per minute for a call to a destination.

And the CTF in the P-GW/PCEF has to know the cost per octet transferred across the network for the subscriber.

In previous generations of mobile networks it may have been desirable to perform decentralized rating, as you can spread the load of calculating our the pricing, however today Centralized is the most common way to approach this, as ensuring the correct rates are in each network element is a headache.

Centralized Unit Determination

In Centralized Unit Determination the CTF tells the OCS the type of service in the Credit Control Request (Requested Service Units), and the OCS determines the number of non-monetary units of a certain service the subscriber can consume.

The CTF doesn’t request a value, just tells the OCS the service being requested and subscriber, and the OCS works out the values.

For example, the S-CSCF specifies in the Credit Control Request the destination the caller wishes to reach, and the OCS replies with the amount of talk time it will grant.

Or for a subscriber wishing to use data, the P-GW/PCEF sends a Credit Control Request specifying the service is data, and the OCS responds with how much data the subscriber is entitled to use.

De-Centralized Unit Determination

In De-Centralized Unit Determination, the CTF determines how many units are required to start the service, and requests these units from the OCS in the Credit Control Request.

For a data service,the CTF in the P-GW would determine how many data units it is requesting for a subscriber, and then request that many units from the OCS.

For a voice call a S-CSCF may request an initial call duration, of say 5 minutes, from the OCS. So it provides the information about the destination and the request for 300 seconds of talk time.

Session Charging with Unit Reservation (SCUR)

Arguably the most common online charging scenario is Session Charging with Unit Reservation (SCUR).

SCUR relies on reserving an amount of funds from the subscriber’s balance, so no other services can those funds and translating that into connectivity units (minutes of talk time or data in/out based on the Requested Session Unit) at the start of the session, and then subsequent requests to debit the reserved amount and reserve a new amount, until all the credit is used.

This uses centralized Unit Determination and centralized Rating.

Let’s take a look at how this would look for the CTF in a P-GW/PCEF performing online charging for a subscriber wishing to use data:

  1. Session Request: The subscriber has attached to the network and is requesting service.
  2. The CTF built into the P-GW/PCEF sends a Credit Control Request: Initial Request (As this subscriber has just attached) to the OCS, with Requested Service Units (RSU) of data in/out to the OCS.
  3. The OCS performs rating and unit determination, and according to it’s credit risk policies, and a whole lot of other factors, comes back with an amount of data the subscriber can use, and reserves the amount from the account.
    (It’s worth noting at this point that this is not necessarily all of the subscriber’s credit in the form of data, just an amount the OCS is willing to allocate. More data can be requested once this allocated data is used up.)
  4. The OCS sends a Credit Control Answer back to our P-GW/PCEF. This contains the Granted Service Unit (GSU), in our case the GSU is data so defines much data up/down the user can transfer. It also may include a Validity Time (VT), which is the number of seconds the Credit Control Answer is valid for, after it’s expired another Credit Control Request must be sent by the CTF.
  5. Our P-GW/PCEF processes this, starts measuring the data used by the subscriber for reporting later, and sets a timer for the Validity Time to send another CCR at that point.
    At this stage, our subscriber is able to start using data.
  1. Some time later, either when all the data allocated in the Granted Service Units has been consumed, or when the Validity Time has expired, the CTF in the P-GW/PCEF sends another Credit Control Request: Update, and again includes the RSU (Requested Service Units) as data in/out, and also a USU (Used Service Units) specifying how much data the subscriber has used since the first Credit Control Answer.
  2. The OCS receives this information. It compares the Used Session Units to the Granted Session Units from earlier, and with this is able to determine how much data the subscriber has actually used, and therefore how much credit that equates to, and debit that amount from the account.
    With this information the OCS can reserve more funds and allocate another GSU (Granted Session Unit) if the subscriber has the required balance. If the subscriber only has a small amount of credit left the FUI (Final Unit Indication AVP) is set to determine this is all the subscriber has left in credit, and if this is exhausted to end the session, rather than sending another Credit Control Request.
  3. The Credit Control Answer with new GSU and the FUI is sent back to the P-GW/PCEF
  4. The P-GW/PCEF allows the session to continue, again monitoring used traffic against the GSU (Granted Session Units).
  1. Once the subscriber has used all the data in the Granted Session Units, and as the last CCA included the Final Unit Indicator, the CTF in the P-GW/PCEF knows it can’t just request more credit in the form of a CCR Update, so cuts of the subscribers’s session.
  2. The P-GW/PCEF then sends a Credit Control Request: Termination Request with the final Used Service Units to the OCS.
  3. The OCS debits the used service units from the subscriber’s balance, and refunds any unused credit reservation.
  4. The OCS sends back a Credit Control Answer which may include the CI value for Credit Information, to denote the cost information which may be passed to the subscriber if required.
Credit Control Request / Answer call flow in IMS Charging

Basics of EPC/LTE Online Charging (OCS)

Early on as subscriber trunk dialing and automated time-based charging was introduced to phone networks, engineers were faced with a problem from Payphones.

Previously a call had been a fixed price, once the caller put in their coins, if they put in enough coins, they could dial and stay on the line as long as they wanted.

But as the length of calls began to be metered, it means if I put $3 of coins into the payphone, and make a call to a destination that costs $1 per minute, then I should only be allowed to have a 3 minute long phone call, and the call should be cutoff before the 4th minute, as I would have used all my available credit.

Conversely if I put $3 into the Payphone and only call a $1 per minute destination for 2 minutes, I should get $1 refunded at the end of my call.

We see the exact same problem with prepaid subscribers on IMS Networks, and it’s solved in much the same way.

In LTE/EPC Networks, Diameter is used for all our credit control, with all online charging based on the Ro interface. So let’s take a look at how this works and what goes on.

Generic 3GPP Online Charging Architecture

3GPP defines a generic 3GPP Online charging architecture, that’s used by IMS for Credit Control of prepaid subscribers, but also for prepaid metering of data usage, other volume based flows, as well as event-based charging like SMS and MMS.

Network functions that handle chargeable services (like the data transferred through a P-GW or calls through a S-CSCF) contain a Charging Trigger Function (CTF) (While reading the specifications, you may be left thinking that the Charging Trigger Function is a separate entity, but more often than not, the CTF is built into the network element as an interface).

The CTF is a Diameter application that generates requests to the Online Charging Function (OCF) to be granted resources for the session / call / data flow, the subscriber wants to use, prior to granting them the service.

So network elements that need to charge for services in realtime contain a Charging Trigger Function (CTF) which in turn talks to an Online Charging Function (OCF) which typically is part of an Online Charging System (AKA OCS).

For example when a subscriber turns on their phone and a GTP session is setup on the P-GW/PCEF, but before data is allowed to flow through it, a Diameter “Credit Control Request” is generated by the Charging Trigger Function (CTF) in the P-GW/PCEF, which is sent to our Online Charging Server (OCS).

The “Credit Control Answer” back from the OCS indicates the subscriber has the balance needed to use data services, and specifies how much data up and down the subscriber has been granted to use.

The P-GW/PCEF grants service to the subscriber for the specified amount of units, and the subscriber can start using data.

This is a simplified example – Decentralized vs Centralized Rating and Unit Determination enter into this, session reservation, etc.

The interface between our Charging Trigger Functions (CTF) and the Online Charging Functions (OCF), is the Ro interface, which is a Diameter based interface, and is common not just for online charging for data usage, IMS Credit Control, MMS, value added services, etc.

3GPP define a reference online-charging interface, the Ro interface, and all the application-specific interfaces, like the Gy for billing data usage, build on top of the Ro interface spec.

Basic Credit Control Request / Credit Control Answer Process

This example will look at a VoLTE call over IMS.

When a subscriber sends an INVITE, the Charging Trigger Function baked in our S-CSCF sends a Diameter “Credit Control Request” (CCR) to our Online Charging Function, with the type INITIAL, meaning this is the first CCR for this session.

The CCR contains the Service Information AVP. It’s this little AVP that is where the majority of the magic happens, as it defines what the service the subscriber is requesting. The main difference between the multitude of online charging interfaces in EPC networks, is just what the service the customer is requesting, and the specifics of that service.

For this example it’s a voice call, so this Service Information AVP contains a “IMS-Information” AVP. This AVP defines all the parameters for a IMS phone call to be online charged, for a voice call, this is the called-party, calling party, SDP (for differentiating between voice / video, etc.).

It’s the contents of this Service Information AVP the OCS uses to make decision on if service should be granted or not, and how many service units to be granted. (If Centralized Rating and Unit Determination is used, we’ll cover that in another post)
The actual logic, relating to this decision is typically based on the the rating and tariffing, credit control profiles, etc, and is outside the scope of the interface, but in short, the OCS will make a yes/no decision about if the subscriber should be granted access to the particular service, and if yes, then how many minutes / Bytes / Events should be granted.

In the received Credit Control Answer is received back from our OCS, and the Granted-Service-Unit AVP is analysed by the S-CSCF.
For a voice call, the service units will be time. This tells the S-CSCF how long the call can go on before the S-CSCF will need to send another Credit Control Request, for the purposes of this example we’ll imagine the returned value is 600 seconds / 10 minutes.

The S-CSCF will then grant service, the subscriber can start their voice call, and start the countdown of the time granted by the OCS.

As our chatty subscriber stays on their call, the S-CSCF approaches the limit of the Granted Service units from the OCS (Say 500 seconds used of the 600 seconds granted).
Before this limit is reached the S-CSCF’s CTF function sends another Credit Control Request with the type UPDATE_REQUEST. This allows the OCS to analyse the remaining balance of the subscriber and policies to tell the S-CSCF how long the call can continue to proceed for in the form of granted service units returned in the Credit Control Answer, which for our example can be 300 seconds.

Eventually, and before the second lot of granted units runs out, our subscriber ends the call, for a total talk time of 700 seconds.

But wait, the subscriber been granted 600 seconds for our INITIAL request, and a further 300 seconds in our UPDATE_REQUEST, for a total of 900 seconds, but the subscriber only used 700 seconds?

The S-CSCF sends a final Credit Control Request, this time with type TERMINATION_REQUEST and lets the OCS know via the Used-Service-Unit AVP, how many units the subscriber actually used (700 seconds), meaning the OCS will refund the balance for the gap of 200 seconds the subscriber didn’t use.

If this were the interface for online charging of data, we’d have the PS-Information AVP, or for online charging of SMS we’d have the SMS-Information, and so on.

The architecture and framework for how the charging works doesn’t change between a voice call, data traffic or messaging, just the particulars for the type of service we need to bill, as defined in the Service Information AVP, and the OCS making a decision on that based on if the subscriber should be granted service, and if yes, how many units of whatever type.

Diameter – Insert Subscriber Data Request / Response

While we’ve covered the Update Location Request / Response, where an MME is able to request subscriber data from the HSS, what about updating a subscriber’s profile when they’re already attached? If we’re just relying on the Update Location Request / Response dialog, the update to the subscriber’s profile would only happen when they re-attach.

We need a mechanism where the HSS can send the Request and the MME can send the response.

This is what the Insert Subscriber Data Request/Response is used for.

Let's imagine we want to allow a subscriber to access an additional APN, or change an AMBR values of an existing APN;

We'd send an Insert Subscriber Data Request from the HSS, to the MME, with the Subscription Data AVP populated with the additional APN the subscriber can now access.

Beyond just updating the Subscription Data, the Insert Subscriber Data Request/Response has a few other funky uses.

Through it the HSS can request the EPS Location information of a Subscriber, down to the TAC / eNB ID serving that subscriber. It’s not the same thing as the GMLC interfaces used for locating subscribers, but will wake Idle UEs to get their current serving eNB, if the Current Location Request is set in the IDR Flags.

But the most common use for the Insert-Subscriber-Data request is to modify the Subscription Profile, contained in the Subscription-Data AVP,

If the All-APN-Configurations-Included-Indicator is set in the AVP info, then all the existing AVPs will be replaced, if it’s not then everything specified is just updated.

The Insert Subscriber Data Request/Response is a bit novel compared to other S6a requests, in this case it’s initiated by the HSS to the MME (Like the Cancel Location Request), and used to update an existing value.

Diameter Agents

Let’s take a look at each of the common Diameter agent variants in use today:

Diameter Relay Agent / Diameter Routing Agent (DRA)

This is the simplest of the Diameter agents, but also probably the most common. The Diameter Relay agent does not look at the contents of the AVPs, it just routes messages based on the Application ID or Destination realm.

A Diameter Relay Agent does not change any AVPs except routing AVPs.

DRAs are transaction aware, but not dialog aware. This means they know if the Diameter request made it to the destination, but have no tracking of getting a response.

DRAs are common as a central hub for all Diameter hub in a network. This allows for a star topology where every Diameter service connects to a central DRA (typically two DRAs for redundancy) for a central place to manage Diameter routing, instead of having to do a full-mesh topology, which would be a nightmare on larger networks.

I recently wrote about creating a simple but unstable DRA with Kamailio.

Diameter Edge Agent

A Diameter Edge Agent is a special DRA that sits on the border between two networks and acts as a gateway between them.

Imagine a roaming exchange scenario, where each operator has to expose their core Diameter servers or DRAs to all the other operators they have roaming agreements with. Like we saw with the DRA to do a full-mesh style connection arrangement would be a mess, and wouldn’t allow internal changes inside the network without significant headaches.

Instead by putting a Diameter Edge Agent at the edge of the network, the operators who wish to access our Diameter information for roaming, only need to connect to a single point, and we can change whatever we like on the inside of the network, adding and removing servers, without having to update our roaming information (IR 21).

We can also strictly enforce security policies on rate limits and admission control, centrally, for all connections in from other operators.

Diameter Proxy Agent

The Diameter Proxy Agent does everything a DRA does, and more!

The Diameter Proxy Agent is application aware, meaning it can decode the AVPs and make decisions based upon the contents of the AVPs. It’s also able to edit / add / delete AVPs and Sub-AVPs.

These are useful for interconnect scenarios where you might need to re-write the value of an AVP, or translate a realm etc, on a Diameter request/response journey.

Diameter Translation Agent

Diameter Translation agents are used for translating between protocols, for example Diameter into MAP for GSM authentication, or into HTTP for 5G authentication.

For 5GC a new network element – the “Binding Support Function” (BSF) is introduced to translate between HTTP for 5G and Diameter for LTE, however this can be thought of as another Diameter Translation Agent.

SCTP Parameter Tuning

There’s a lot to like about SCTP. No head of line blocking, MTU issues, sequenced, acknowledged delivery of messages, not to mention Multi-Homing and message bundling.

But if you really want to get the most bang for your buck, you’ll need to tune your SCTP parameters to match the network conditions.

While tuning the parameters per-association would be time consuming, most SCTP stacks allow you to set templates for SCTP parameters, for example you would have a different set of parameters for the SCTP stacks inside your network, compared to SCTP stacks for say a roaming scenario or across microwave links.

IETF kindly provides a table with their recommended starting values for SCTP parameter tuning:

RTO.Initial3 seconds
RTO.Min1 second
RTO.Max60 seconds
Max.Burst4
RTO.Alpha1/8
RTO.Beta1/4
Valid.Cookie.Life60 seconds
Association.Max.Retrans10 attempts
Path.Max.Retrans5 attempts (per destination address)
Max.Init.Retransmits8 attempts
HB.interval30 seconds
HB.Max.Burst1
IETF – RFC4960: SCTP – Suggested Protocol Parameter Values

But by adjusting the Max Retrans and Retransmission Timeout (RTO) values, we can detect failures on the network more quickly, and reduce the number of packets we’ll loose should we have a failure.

We begin with the engineered round-trip time (RTT) – that is made up of the time it takes to traverse the link, processing time for the remote SCTP stack and time for the response to traverse the link again. For the examples below we’ll take an imaginary engineered RTT of 200ms.

RTO.min is the minimum retransmission timeout.
If this value is set too low then before the other side has had time to receive the request, process it and send a response, we’ve already retransmitted it.

This should be set to the round trip delay plus processing needed to send and acknowledge a packet plus some allowance for variability due to jitter; a value of 1.15 times the Engineered RTT is often chosen

So for us, 200 * 1.15 = 230ms RTO.min value.

RTO.max is the maximum amount of time we should wait before transmitting a request.
Typically three times the Engineered RTT.

So for us, 200 * 3 = 600ms RTO.min value.

Path.Max.Retransmissions is the maximum number of retransmissions to be sent down a path before the path is considered to be failed.
For example if we loose a transmission path on a multi-homed server, how many retransmissions along that path should we send until we consider it to be down?

Values set are dependant on if you’re multi-homing or not (you can be more picky if you are) and the level of acceptable packet loss in your transmission link.

Typical values are 4 Retransmissions (per destination address) for a Single-Homed association, and 2 Retransmissions (per destination address) for a Multi-Homed association.

Association.Max.Retransmissions is the maximum number of retransmissions for an association. If a transmission link in a multi-homed SCTP scenario were to go down, we would pass the Path.Max.Retransmissions value and the SCTP stack would stop sending traffic out that path, and try another, but what if the remote side is down? In that scenario all our paths would fail, so we need another counter – Path.Max.Retransmissions to count the total number of retransmissions to an association / destination. When the Association.Max.Retransmissions is reached the association is considered down.

In practice this value would be the number of paths, multiplied by the Path.Max.Retransmissions.

IMS Routing with iFCs

SIP routing is complicated, there’s edge cases, traffic that can be switched locally and other traffic that needs to be proxied off to another Proxy or Application server. How can you define these rules and logic in a flexible way, that allows these rules to be distributed out to multiple different network elements and adjusted on a per-subscriber basis?

Enter iFCs – The Initial Filter Criteria.

iFCs are XML encoded rules to define which servers should handle traffic matching a set of rules.

Let’s look at some example rules we might want to handle through iFCs:

  • Send all SIP NOTIFY, SUBSCRIBE and PUBLISH requests to a presence server
  • Any Mobile Originated SMS to an SMSc
  • Calls to a specific destination to a MGC
  • Route any SIP INVITE requests with video codecs present to a VC bridge
  • Send calls to Subscribers who aren’t registered to a Voicemail server
  • Use 3rd party registration to alert a server that a Subscriber has registered

All of these can be defined and executed through iFCs, so let’s take a look,

iFC Structure

iFCs are encoded in XML and typically contained in the Cx-user-data AVP presented in a Cx Server Assignment Answer response.

Let’s take a look at an example iFC and then break down the details as to what we’re specifying.

<InitialFilterCriteria>
    <Priority>10</Priority>
    <TriggerPoint>
        <ConditionTypeCNF>1</ConditionTypeCNF>
        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>0</Group>
            <Method>MESSAGE</Method>
        </SPT>
        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>1</Group>
            <SessionCase>0</SessionCase>
        </SPT>
    </TriggerPoint>
    <ApplicationServer>
        <ServerName>sip:smsc.mnc001.mcc001.3gppnetwork.org:5060</ServerName>
        <DefaultHandling>0</DefaultHandling>
    </ApplicationServer>
</InitialFilterCriteria>

Each rule in an iFC is made up of a Priority, TriggerPoint and ApplicationServer.

So for starters we’ll look at the Priority tag.
The Priority tag allows us to have multiple-tiers of priority and multiple levels of matching,
For example if we had traffic matching the conditions outlined in this rule (TriggerPoint) but also matching another rule with a lower priority, the lower priority rule would take precedence.

Inside our <TriggerPoint> tag contains the specifics of the rules and how the rules will be joined / matched, which is what we’ll focus on predominantly, and is followed by the <ApplicationServer> which is where we will route the traffic to if the TriggerPoint is matched / triggered.

So let’s look a bit more about what’s going on inside the TriggerPoint.

Each TriggerPoint is made up of Service Point Trigger (SPTs) which are individual rules that are matched or not matched, that are either combined as logical AND or logical OR statements when evaluated.

By using fairly simple building blocks of SPTs we can create a complex set of rules by joining them together.

Service Point Triggers (SPTs)

Let’s take a closer look at what goes on in an SPT.
Below is a simple SPT that will match all SIP requests using the SIP MESSAGE method request type:

        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>0</Group>
            <Method>MESSAGE</Method>
        </SPT>

So as you may have guessed, the <Method> tag inside the SPT defines what SIP request method we’re going to match.

But Method is only one example of the matching mechanism we can use, but we can also match on other attributes, such as Request URI, SIP Header, Session Case (Mobile Originated vs Mobile Terminated) and Session Description such as SDP.

Or an example of a SPT for anything Originating from the Subscriber utilizing the <SessionCase> tag inside the SPT.

        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>0</Group>
            <SessionCase>0</SessionCase>
        </SPT>

Below is another SPT that’s matching any requests where the request URI is sip:[email protected] by setting the <RequestURI> tag inside the SPT:

        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>0</Group>
            <RequestURI>sip:[email protected]</RequestURI>
        </SPT>

We can match SIP headers, either looking for the existence of a header or the value it is set too,

        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>0</Group>
            <SIPHeader>
              <Header>To</Header>
              <Content>"Nick"</Content>
            </SIPHeader>
        </SPT>

Having <Header> will match if the header is present, while the optional Content tag can be used to match

In terms of the Content this is matched using Regular Expressions, but in this case, not so regular regular expressions. 3GPP selected Extended Regular Expressions (ERE) to be used (IEEE POSIX) which are similar to the de facto standard PCRE Regex, but with a few fewer parameters.

Condition Negated

The <ConditionNegated> tag inside the SPT allows us to do an inverse match.

In short it will match anything other than what is specified in the SPT.

For example if we wanted to match any SIP Methods other than MESSAGE, setting <ConditionNegated>1</ConditionNegated> would do just that, as shown below:

        <SPT>
            <ConditionNegated>1</ConditionNegated>
            <Group>0</Group>
            <Method>MESSAGE</Method>
        </SPT>

And another example of ConditionNegated in use, this time we’re matching anything where the Request URI is not sip:[email protected]:

        <SPT>
            <ConditionNegated>1</ConditionNegated>
            <Group>0</Group>
            <RequestURI>sip:[email protected]</RequestURI>
        </SPT>

Finally the <Group> tag allows us to group together a group of rules for the purpose of evaluating.
We’ll go into it more in in the below section.

ConditionTypeCNF / ConditionTypeDNF

As we touched on earlier, <TriggerPoints> contain all the SPTs, but also, very importantly, specify how they will be interpreted.

SPTs can be joined in AND or OR conditions.

For some scenarios we may want to match where METHOD is MESSAGE and RequestURI is sip:[email protected], which is different to matching where the METHOD is MESSAGE or RequestURI is sip:[email protected].

This behaviour is set by the presence of one of the ConditionTypeCNF (Conjunctive Normal Form) or ConditionTypeDNF (Disjunctive Normal Form) tags.

If each SPT has a unique number in the GroupTag and ConditionTypeCNF is set then we evaluate as AND.

If each SPT has a unique number in the GroupTag and ConditionTypeDNF is set then we evaluate as OR.

Let’s look at how the below rule is evaluated as AND as ConditionTypeCNF is set:

<InitialFilterCriteria>
    <Priority>10</Priority>
    <TriggerPoint>
        <ConditionTypeCNF>1</ConditionTypeCNF>
        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>0</Group>
            <Method>MESSAGE</Method>
        </SPT>
        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>1</Group>
            <SessionCase>0</SessionCase>
        </SPT>
    </TriggerPoint>
    <ApplicationServer>
        <ServerName>sip:smsc.mnc001.mcc001.3gppnetwork.org:5060</ServerName>
        <DefaultHandling>0</DefaultHandling>
    </ApplicationServer>
</InitialFilterCriteria>

This means we will match if the method is MESSAGE and Session Case is 0 (Mobile Originated) as each SPT is in a different Group which leads to “and” behaviour.

If we were to flip to ConditionTypeDNF each of the SPTs are evaluated as OR.

<InitialFilterCriteria>
    <Priority>10</Priority>
    <TriggerPoint>
        <ConditionTypeDNF>1</ConditionTypeDNF>
        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>0</Group>
            <Method>MESSAGE</Method>
        </SPT>
        <SPT>
            <ConditionNegated>0</ConditionNegated>
            <Group>1</Group>
            <SessionCase>0</SessionCase>
        </SPT>
    </TriggerPoint>
    <ApplicationServer>
        <ServerName>sip:smsc.mnc001.mcc001.3gppnetwork.org:5060</ServerName>
        <DefaultHandling>0</DefaultHandling>
    </ApplicationServer>
</InitialFilterCriteria>

This means we will match if the method is MESSAGE and Session Case is 0 (Mobile Originated).

Where this gets a little bit more complex is when we have multiple entries in the same Group tag.

Let’s say we have a trigger point made up of:

<SPT><Method>MESSAGE</Method><Group>1</Group></SPT>
<SPT><SessionCase>0</SessionCase><Group>1</Group></SPT> 

<SPT><Header>P-Some-Header</Header><Group>2</Group></SPT> 

How would this be evaluated?

If we use ConditionTypeDNF every SPT inside the same Group are matched as AND, and SPTs with distinct are matched as OR.

Let’s look at our example rule evaluated as ConditionTypeDNF:

<ConditionTypeDNF>1</ConditionTypeDNF>
  <SPT><Method>MESSAGE</Method><Group>1</Group></SPT>
  <SPT><SessionCase>0</SessionCase><Group>1</Group></SPT> 

  <SPT><Header>P-Some-Header</Header><Group>2</Group></SPT> 

This means the two entries in Group 1 are evaluated as AND – So Method is message and Session Case is 0, OR the header “P-Some-Header” is present.

Let’s do another one, this time as ConditionTypeCNF:

<ConditionTypeCNF>1</ConditionTypeCNF>
  <SPT><Method>MESSAGE</Method><Group>1</Group></SPT>
  <SPT><SessionCase>0</SessionCase><Group>1</Group></SPT> 

  <SPT><Header>P-Some-Header</Header><Group>2</Group></SPT> 

This means the two entries in Group 1 are evaluated as OR – So Method is message OR Session Case is 0, AND the header “P-Some-Header” is present.

Diameter Droplets – The Flow-Description AVP and IPFilterRules

When it comes to setting up dedicated bearers, the Flow-Description AVP is perhaps the most important,

The specially encoded string (IPFilterRule) in the FlowDescription AVP is what our P-GW (Ok, our PCEF) uses to create Traffic Flow Templates to steer certain types of traffic down Dedicated Bearers.

So let’s take a look at how we can lovingly craft an artisanal Flow-Description.

The contents of the AVP are technically not a string, but a IPFilterRule.

IPFilterRules are actually defined in the Diameter Base Protocol (IETF RFC 6733), where we can learn the basics of encoding them,

Which are in turn based loosely off the ipfw utility in BSD.

They take the format:

action dir proto from src to dst

The action is fairly simple, for all our Dedicated Bearer needs, and the Flow-Description AVP, the action is going to be permit. We’re not blocking here.

The direction (dir) in our case is either in or out, from the perspective of the UE.

Next up is the protocol number (proto), as defined by IANA, but chances are you’ll be using 17 (UDP) or 6 (TCP) in most scenarios.

The from value is followed by an IP address with an optional subnet mask in CIDR format, for example from 10.45.0.0/16 would match everything in the 10.45.0.0/16 network.
Following from you can also specify the port you want the rule to apply to, or, a range of ports,
For example to match a single port you could use 10.45.0.0/16 1234 to match anything on port 1234, but we can also specify ranges of ports like 10.45.0.0/16 0 – 4069 or even mix and match lists and single ports, like 10.45.0.0/16 5060, 1000-2000

Protip: using any is the same as 0.0.0.0/0

Like the from, the to is encoded in the same way, with either a single IP, or a subnet, and optional ports specified.

And that’s it!

Keep in mind that Flow-Descriptions are typically sent in pairs as a minimum, as you want to match the traffic into and out of the network (not just one way), but often there can be quite a few sent, in order to match all the possible traffic that needs to be matched that may be across multiple different subnets, etc.

There is an optional Options parameter that allows you to set things like to only apply the rule to open TCP sessions, fragmentation, etc, although I’ve not seen this implemented in the wild.

Example IP filter Rules

permit in 6 from 10.98.254.0/24 5061 to 10.98.0.0/24 5060
permit out 6 from 10.98.254.0/24 5060 to 10.98.0.0/24 5061

permit in 6 from any 80 to 172.16.1.1 80
permit out 6 from 172.16.1.1 80 to any 80

permit in 17 from 10.98.254.0/24 50000-60100 to 10.98.0.0/24 50000-60100
permit out 17 from 10.98.254.0/24 50000-60100 to 10.98.0.0/24 50000-60100

permit in 17 from 10.98.254.0/24 5061, 5064 to 10.98.0.0/24  5061, 5064
permit out 17 from 10.98.254.0/24 5061, 5064 to 10.98.0.0/24  5061, 5064

permit in 17 from 172.16.0.0/16 50000-60100, 5061, 5064 to 172.16.0.0/16  50000-60100, 5061, 5064
permit out 17 from 172.16.0.0/16 50000-60100, 5061, 5064 to 172.16.0.0/16  50000-60100, 5061, 5064

For more info see:

RFC 6773 – Diameter Base Protocol – IP Filter Rule

3GPP TS 29.214 section 5.3.8 Flow-Description AVP