Tag Archives: Diameter

VoLTE / IMS – Analysis Challenge

It’s challenge time, this time we’re going to be looking at an IMS PCAP, and answering some questions to test your IMS analysis chops!

Here’s the packet capture:

Easy Questions

  • What QCI value is used for the IMS bearer?
  • What is the registration expiry?
  • What is the E-UTRAN Cell ID the Subscriber is served by?
  • What is the AMBR of the IMS APN?

Intermediate Questions

  • Is this the first or subsequent registration?
  • What is the Integrity-Key for the registration?
  • What is the FQDN of the S-CSCF?
  • What Nonce value is used and what does it do?
  • What P-CSCF Addresses are returned?
  • What time would the UE need to re-register by in order to stay active?
  • What is the AA-Request in #476 doing?
  • Who is the(opens in a new tab)(opens in a new tab)(opens in a new tab) OEM of the handset?
  • What is the MSISDN associated with this user?

Hard Questions

  • What port is used for the ESP data?
  • Which encryption algorithm and algorithm is used?
  • How many packets are sent over the ESP tunnel to the UE?
  • Where should SIP SUBSCRIBE requests get routed?
  • What’s the model of phone?

The answers for each question are on the next page, let me know in the comments how you went, and if there’s any tricky ones!

What’s the maximum speed for LTE and 5G?

Even before 5G was released, the arms race to claim the “fastest” speeds on LTE, NSA and SA networks has continued, with pretty much every operator claiming a “first” or “fastest”.

I myself have the fastest 5G network available* but I thought I’d look at how big the values are we can put in for speed, these are the Maximum Bitrate Values (like AMBR) we can set on an APN/DNN, or on a Charging Rule.

*Measurement is of the fastest 5G network in an eastward facing office, operated by a person named Nick, in a town in Australia. Other networks operated by people other than those named Nick in eastward facing office outside of Australia were not compared.

The answer for Release 8 LTE is 4294967294 bytes per second, aka 4295 Mbps 4.295 Gbps.

Not bad, but why this number?

The Max-Requested-Bandwidth-DL AVP tells the PGW the max throughput allowed in bits per second. It’s a Unsigned32 so max value is 4294967294, hence the value.

But come release 15 some bright spark thought we may in the not to distant future break this barrier, so how do we go above this?

The answer was to bolt on another AVP – the “Extended-Max-Requested-BW-DL” AVP ( 554 ) was introduced, you might think that means the max speed now becomes 2x 4.295 Gbps but that’s not quite right – The units was shifted.

This AVP isn’t measuring bits per second it’s measuring kilobits per second.

So the standard Max-Requested-Bandwidth-DL AVP gives us 4.3 Gbps, while the Extended-Max-Requested-Bandwidth gives us a 4,295 Gbps.

We add the Extended-Max-Requested-Bandwidth AVP (4295 Gbps) onto the Max-Requested Bandwidth AVP (4.3 Gbps) giving us a total of 4,4299.3 Gbps.

So the short answer:

Pre release 15: 4.3 Gbps

Post release 15: 4,4299.3 Gbps

Authenticating Fixed Line Subscribers into IMS

We recently added support in PyHSS for fixed line SIP subscribers to attach to the IMS.

Traditional telecom operators are finding their fixed line network to be a bit of a money pit, something they’re required to keep operating to meet regulatory obligations, but the switches are sitting idle 99% of the time. As such we’re seeing more and more operators move fixed line subs onto their IMS.

This new feature means we can use PyHSS to serve as the brains for a fixed network, as well as for mobile, but there’s one catch – How we authenticate subscribers changes.

Most banks of line cards in a legacy telecom switches, or IP Phones, don’t have SIM slots to allow us to authenticate, so instead we’re forced to fallback to what they do support.

Unfortunately for the most part, what is supported by these IP phones or telecom switches is SIP MD5 Digest Authentication.

The Nonce is generated by the HSS and put into the Multimedia-Authentication-Answer, along with the subscriber’s password and sent in the clear to the S-CSCF.

Subscriber with Password made up of all 1's MAA response from HSS for Digest-MD5 Auth

The HSS then generates the the Multimedia-Auth Answer, it generates a nonce (in the 3GPP-SIP-Authenticate / 609 AVP) and sends the Subscriber’s password in the 3GPP-SIP-Authorization (610) AVP in response back to the S-CSCF.

I would have thought a better option would be for the HSS to generate the Nonce and Digest, and then the S-CSCF to just send the Nonce to the Sub and compare the returned Digest from the Sub against the expected Digest from the HSS, but it would limit flexibility (realm adaptation, etc) I guess.

The UE/UA (I guess it’s a UA in this context as it’s not a mobile) then generates its own Digest from the Nonce and sends it back to the S-CSCF via the P-CSCF.

The S-CSCF compares the received Digest response against the one it generated, and if the two match, the sub is authenticated and allowed to attach onto the network.

SQN Sync in IMS Auth

So the issue was a head scratcher.

Everything was working on the IMS, then I go to bed, the next morning I fire up the test device and it just won’t authenticate to the IMS – The S-CSCF generated a 401 in response to the REGISTER, but the next REGISTER wouldn’t pass.

Wireshark just shows me this loop:

UE -> IMS: REGISTER
IMS -> UE: 401 Unauthorized (With Challenge)
UE -> IMS: REGISTER with response
IMS -> UE: 401 Unauthorized (With Challenge)
UE -> IMS: REGISTER with response
IMS -> UE: 401 Unauthorized (With Challenge)
UE -> IMS: REGISTER with response
IMS -> UE: 401 Unauthorized (With Challenge)

So what’s going on here?

IMS uses AKAv1-MD5 for Authentication, this is slightly different to the standard AKA auth used in cellular, but if you’re curious, we’ve covered by IMS Authentication and standard AKA based SIM Authentication in cellular networks before.

When we generate the vectors (for IMS auth and standard auth) one of the inputs to generate the vectors is the Sequence Number or SQN.

This SQN ticks over like an odometer for the number of times the SIM / HSS authentication process has been performed.

There is some leeway in the SQN – It may not always match between the SIM and the HSS and that’s to be expected.
When the MME sends an Authentication-Information-Request it can ask for multiple vectors so it’s got some in reserve for the next time the subscriber attaches, and that’s allowed.

Information stored on USIM / SIM Card for LTE / EUTRAN / EPC - K key, OP/OPc key and SQN Sequence Number

But there are limits to how far out our SQN can be, and for good reason – One of the key purposes for the SQN is to protect against replay attacks, where the same vector is replayed to the UE. So the SQN on the HSS can be ahead of the SIM (within reason), but it can’t be behind – Odometers don’t go backwards.

So the issue was with the SQN on the SIM being out of Sync with the SQN in the IMS, how do we know this is the case, and how do we fix this?

Well there is a resync mechanism so the SIM can securely tell the HSS what the current SQN it is using, so the HSS can update it’s SQN.

When verifying the AUTN, the client may detect that the sequence numbers between the client and the server have fallen out of sync.
In this case, the client produces a synchronization parameter AUTS, using the shared secret K and the client sequence number SQN.
The AUTS parameter is delivered to the network in the authentication response, and the authentication can be tried again based on authentication vectors generated with the synchronized sequence number.

RFC 3110: HTTP Digest Authentication using AKA

In our example we can tell the sub is out of sync as in our Multimedia Authentication Request we see the SIP-Authorization AVP, which contains the AUTS (client synchronization parameter) which the SIM generated and the UE sent back to the S-CSCF. Our HSS can use the AUTS value to determine the correct SQN.

SIP-Authorization AVP in the Multimedia Authentication Request means the SQN is out of Sync and this AVP contains the RAND and AUTN required to Resync

Note: The SIP-Authorization AVP actually contains both the RAND and the AUTN concatenated together, so in the above example the first 32 bytes are the AUTN value, and the last 32 bytes are the RAND value.

So the HSS gets the AUTS and from it is able to calculate the correct SQN to use.

Then the HSS just generates a new Multimedia Authentication Answer with a new vector using the correct SQN, sends it back to the IMS and presto, the UE can respond to the challenge normally.

This feature is now fully implemented in PyHSS for anyone wanting to have a play with it and see how it all works.

And that friends, is how we do SQN resync in IMS!

Getting to know the PCRF for traffic Policy, Rules & Rating

Misunderstood, under appreciated and more capable than people give it credit for, is our PCRF.

But what does it do?

Most folks describe the PCRF in hand wavy-terms – “it does policy and charging” is the answer you’ll get, but that doesn’t really tell you anything.

So let’s answer it in a way that hopefully makes some practical sense, starting with the acronym “PCRF” itself, it stands for Policy and Charging Rules Function, which is kind of two functions, one for policy and one for rules, so let’s take a look at both.

Policy

In cellular world, as in law, policy is the rules.

For us some examples of policy could be a “fair use policy” to limit customer usage to acceptable levels, but it can also be promotional packages, services like “free Spotify” packages, “Voice call priority” or “unmetered access to Nick’s Blog and maximum priority” packages, can be offered to customers.

All of these are examples of policy, and to make them work we need to target which subscribers and traffic we want to apply the policy to, and then apply the policy.

Charging Rules

Charging Rules are where the policy actually gets applied and the magic happens.

It’s where we take our policy and turn it into actionable stuff for the cellular world.

Let’s take an example of “unmetered access to Nick’s Blog and maximum priority” as something we want to offer in all our cellular plans, to provide access that doesn’t come out of your regular usage, as well as provide QCI 5 (Highest non dedicated QoS) to this traffic.

To achieve this we need to do 3 things:

  • Profile the traffic going to this website (so we capture this traffic and not regular other internet traffic)
  • Charge it differently – So it’s not coming from the subscriber’s regular balance
  • Up the QoS (QCI) on this traffic to ensure it’s high priority compared to the other traffic on the network

So how do we do that?

Profiling Traffic

So the first step we need to take in providing free access to this website is to filter out traffic to this website, from the traffic not going to this website.

Let’s imagine that this website is hosted on a single machine with the IP 1.2.3.4, and it serves traffic on TCP port 443. This is where IPFilterRules (aka TFTs or “Traffic Flow Templates”) and the Flow-Description AVP come into play. We’ve covered this in the past here, but let’s recap:

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

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).

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.

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!

So let’s create a rule that matches all traffic to our website hosted on 1.2.3.4 TCP port 443,

permit out 6 from 1.2.3.4 443 to any 1-65535
permit out 6 from any 1-65535 to 1.2.3.4 443

All this info gets put into the Flow-Information AVPs:

With the above, any traffic going to/from 1.23.4 on port 443, will match this rule (unless there’s another rule with a higher precedence value).

Charging Actions

So with our traffic profiled, the next question is what actions are we going to take, well there’s two, we’re going to provide unmetered access to the profiled traffic, and we’re going to use QCI 4 for the traffic (because you’ll need a guaranteed bit rate bearer to access!).

Charging-Group for Profiled Traffic

To allow for Zero Rating for traffic matching this rule, we’ll need to use a different Rating Group.

Let’s imagine our default rating group for data is 10000, then any normal traffic going to the OCS will use rating group 10000, and the OCS will apply the specific rates and policies based on that.

Rating Groups are defined in the OCS, and dictate what rates get applied to what Rating Groups.

For us, our default rating group will be charged at the normal rates, but we can define a rating group value of 4000, and set the OCS to provide unlimited traffic to any Credit-Control-Requests that come in with Rating Group 4000.

This is how operators provide services like “Unlimited Facebook” for example, a Charging Rule matches the traffic to Facebook based on TFTs, and then the Rating Group is set differently to the default rating group, and the OCS just allows all traffic on that rating group, regardless of how much is consumed.

Inside our Charging-Rule-Definition, we populate the Rating-Group AVP to define what Rating Group we’re going to use.

Setting QoS for Profiled 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.

As mentioned at the start, we’ll use QCI 4 for this traffic, and allocate MBR/GBR values for this traffic.

Putting it Together – The Charging Rule

So with our TFTs defined to match the traffic, our Rating Group to charge the traffic and our QoS to apply to the traffic, we’re ready to put the whole thing together.

So here it is, our “Free NVN” rule:

I’ve attached a PCAP of the flow to this post.

In our next post we’ll talk about how the PGW handles the installation of this rule.

Kamailio Diameter Routing Agent Support

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

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

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

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

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

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

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

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

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

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

Diameter Routing Agents – Part 5 – AVP Transformations with FreeDiameter and Python in rt_pyform

In our last post we talked about why we’d want to perform Diameter AVP translations / rewriting on our Diameter Routing Agent.

Now let’s look at how we can actually achieve this using rt_pyform extension for FreeDiameter and some simple Python code.

Before we build we’ll need to make sure we have the python3-devel package (I’m using python3-devel-3.10) installed.

Then we’ll build FreeDiameter with the rt_pyform, this branch contains the rt_pyform extension in it already, or you can clone the extension only from this repo.

Now once FreeDiameter is installed we can load the extension in our freeDiameter.conf file:

LoadExtension = "rt_pyform.fdx" : "<Your config filename>.conf";

Next we’ll need to define our rt_pyform config, this is a super simple 3 line config file that specifies the path of what we’re doing:

DirectoryPath = "."        # Directory to search
ModuleName = "script"      # Name of python file. Note there is no .py extension
FunctionName = "transform" # Python function to call

The DirectoryPath directive specifies where we should search for the Python code, and ModuleName is the name of the Python script, lastly we have FunctionName which is the name of the Python function that does the rewriting.

Now let’s write our Python function for the transformation.

The Python function much have the correct number of parameters, must return a string, and must use the name specified in the config.

The following is an example of a function that prints out all the values it receives:

def transform(appId, flags, cmdCode, HBH_ID, E2E_ID, AVP_Code, vendorID, value):
    print('[PYTHON]')
    print(f'|-> appId: {appId}')
    print(f'|-> flags: {hex(flags)}')
    print(f'|-> cmdCode: {cmdCode}')
    print(f'|-> HBH_ID: {hex(HBH_ID)}')
    print(f'|-> E2E_ID: {hex(E2E_ID)}')
    print(f'|-> AVP_Code: {AVP_Code}')
    print(f'|-> vendorID: {vendorID}')
    print(f'|-> value: {value}')
    
    return value

Note the order of the arguments and that return is of the same type as the AVP value (string).

We can expand upon this and add conditionals, let’s take a look at some more complex examples:

def transform(appId, flags, cmdCode, HBH_ID, E2E_ID, AVP_Code, vendorID, value):
    print('[PYTHON]')
    print(f'|-> appId: {appId}')
    print(f'|-> flags: {hex(flags)}')
    print(f'|-> cmdCode: {cmdCode}')
    print(f'|-> HBH_ID: {hex(HBH_ID)}')
    print(f'|-> E2E_ID: {hex(E2E_ID)}')
    print(f'|-> AVP_Code: {AVP_Code}')
    print(f'|-> vendorID: {vendorID}')
    print(f'|-> value: {value}')
    #IMSI Translation - if App ID = 16777251 and the AVP being evaluated is the Username
    if (int(appId) == 16777251) and int(AVP_Code) == 1:
        print("This is IMSI '" + str(value) + "' - Evaluating transformation")
        print("Original value: " + str(value))
        value = str(value[::-1]).zfill(15)

The above look at if the App ID is S6a, and the AVP being checked is AVP Code 1 (Username / IMSI ) and if so, reverses the username, so IMSI 1234567 becomes 7654321, the zfill is just to pad with leading 0s if required.

Now let’s do another one for a Realm Rewrite:

def transform(appId, flags, cmdCode, HBH_ID, E2E_ID, AVP_Code, vendorID, value):

    #Print Debug Info
    print('[PYTHON]')
    print(f'|-> appId: {appId}')
    print(f'|-> flags: {hex(flags)}')
    print(f'|-> cmdCode: {cmdCode}')
    print(f'|-> HBH_ID: {hex(HBH_ID)}')
    print(f'|-> E2E_ID: {hex(E2E_ID)}')
    print(f'|-> AVP_Code: {AVP_Code}')
    print(f'|-> vendorID: {vendorID}')
    print(f'|-> value: {value}')
    #Realm Translation
    if int(AVP_Code) == 283:
        print("This is Destination Realm '" + str(value) + "' - Evaluating transformation")
    if value == "epc.mnc001.mcc001.3gppnetwork.org":
        new_realm = "epc.mnc999.mcc999.3gppnetwork.org"
        print("translating from " + str(value) + " to " + str(new_realm))
        value = new_realm
    else:
        #If the Realm doesn't match the above conditions, then don't change anything
        print("No modification made to Realm as conditions not met")
    print("Updated Value: " + str(value))

In the above block if the Realm is set to epc.mnc001.mcc001.3gppnetwork.org it is rewritten to epc.mnc999.mcc999.3gppnetwork.org, hopefully you can get a handle on the sorts of transformations we can do with this – We can translate any string type AVPs, which allows for hostname, realm, IMSI, Sh-User-Data, Location-Info, etc, etc, to be rewritten.

NB-IoT NIDD Basics

NB-IoT introduces support for NIDD – Non-IP Data Delivery (NIDD) which is one of the cool features of NB-IoT that’s gaining more widespread adoption.

Let’s take a deep dive into NIDD.

The case against IP for IoT

In the over 40 years since IP was standardized, we’ve shoehorned many things onto IP, but IP was never designed or optimized for low power, low throughput applications.

For the battery life of an IoT device to be measured in years, it has to be very selective about what power hungry operations it does. Transmitting data over the air is one of the most power-intensive operations an IoT device can perform, so we need to do everything we can to limit how much data is sent, and how frequently.

Use Case – NB-IoT Tap

Let’s imagine we’re launching an IoT tap that transmits information about water used, as part of our revolutionary new “Water as a Service” model (WaaS) which removes the capex for residents building their own water treatment plant in their homes, and instead allows dynamic scaling of waterloads as they move to our new opex model.

If I turn on the tap and use 12L of water, when I turn off the tap, our IoT tap encodes the usage onto a single byte and sends the usage information to our rain-cloud service provider.

So we’re not constantly changing the batteries in our taps, we need to send this one byte of data as efficiently as possible, so as to maximize the battery life.

If we were to transport our data on TCP, well we’d need a 3 way handshake and several messages just to transmit the data we want to send.

Let’s see how our one byte of data would look if we transported it on TCP.

That sliver of blue in the diagram is our usage component, the rest is overhead used to get it there. Seems wasteful huh?

Sure, TCP isn’t great for this you say, you should use UDP! But even if we moved away from TCP to UDP, we’ve still got the IPv4 header and the UDP header wasting 28 bytes.

For efficiency’s sake (To keep our batteries lasting as long as possible) we want to send as few messages as possible, and where we do have to send messages, keep them very short, so IP is not a great fit here.

Enter NIDD – Non-IP Data Delivery.

Through NIDD we can just send the single hex byte, only be charged for the single hex byte, and only stay transmitting long enough to send this single byte of hex (Plus the NBIoT overheads / headers).

Compared to UDP transport, NIDD provides us a reduction of 28 bytes of overhead for each message, or a 96% reduction in message size, which translates to real power savings for our IoT device.

In summary – the more sending your device has to do, the more battery it consumes.
So in a scenario where you’re trying to maximize power efficiency to keep your batter powered device running as long as possible, needing to transmit 28 bytes of wasted data to transport 1 byte of usable data, is a real waste.

Delivering the Payload

NIDD traffic is transported as raw hex data end to end, this means for our 1 byte of water usage data, the device would just send the hex value to be transferred and it’d pop out the other end.

To support this we introduce a new network element called the SCEFService Capability Exposure Function.

From a developer’s perspective, the SCEF is the gateway to our IoT devices. Through the RESTful API on the SCEF (T8 API), we can send and receive raw hex data to any of our IoT devices.

When one of our Water-as-a-Service Taps sends usage data as a hex byte, it’s the software talking on the T8 API to the SCEF that receives this data.

Data of course needs to be addressed, so we know where it’s coming from / going to, and T8 handles this, as well as message reliability, etc, etc.

This is a telco blog, so we should probably cover the MME connection, the MME talks via Diameter to the SCEF. In our next post we’ll go into these signaling flows in more detail.

If you’re wondering what the status of Open Source SCEF implementations are, then you may have already guessed I’m working on one!

Hopefully by now you’ve got a bit of an idea of how NIDD works in NB-IoT, and in our next posts we’ll dig deeper into the flows and look at some PCAPs together.

Diameter Routing Agents – Part 5 – AVP Transformations

Having a central pair of Diameter routing agents allows us to drastically simplify our network, but what if we want to perform some translations on AVPs?

For starters, what is an AVP transformation? Well it’s simply rewriting the value of an AVP as the Diameter Request/Response passes through the DRA. A request may come into the DRA with IMSI xxxxxx and leave with IMSI yyyyyy if a translation is applied.

So why would we want to do this?

Well, what if we purchased another operator who used Realm X, and we use Realm Y, and we want to link the two networks, then we’d need to rewrite Realm Y to Realm X, and Realm X to Realm Y when they communicate, AVP transformations allow for this.

If we’re an MVNO with hosted IMSIs from an MNO, but want to keep just the one IMSI in our HSS/OCS, we can translate from the MNO hosted IMSI to our internal IMSI, using AVP transformations.

If our OCS supports only one rating group, and we want to rewrite all rating groups to that one value, AVP transformations cover this too.

There are lots of uses for this, and if you’ve worked with a bit of signaling before you’ll know that quite often these sorts of use-cases come up.

So how do we do this with freeDiameter?

To handle this I developed a module for passing each AVP to a Python function, which can then apply any transformation to a text based value, using every tool available to you in Python.

In the next post I’ll introduce rt_pyform and how we can use it with Python to translate Diameter AVPs.

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.
  • 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!