I run Ubuntu on my desktop and I mess with Kamailio a lot.
Recently I was doing some work with KEMI using Python, but installing the latest versions of Kamailio from the Debian Repos for Kamailio wasn’t working.
The following packages have unmet dependencies:
kamailio-python3-modules : Depends: libpython3.11 (>= 3.11.0) but 3.11.0~rc1-1~22.04 is to be installed
Kamailio’s Python modules expect libpython3.11 or higher, but Ubuntu 22.04 repos only contain the release candidate – not the final version:
If you’re used to dealing with SIP, you’d expect to see:
Content-Type: application/sdp
This Content-Type multipart/mixed;boundary is totally valid in SIP, in fact RFC 5261 (Message Body Handling in the Session Initiation Protocol (SIP)) details the use of MIME in SIP, and the Geolocation extension uses this, as we see below from a 911 call example.
But while this extension is standardised, and having your SIP Body containing multipart MIME is legal, not everything supports this, including the FreeSWITCH bridge module, which just appends a new SDP body into the Mime Multipart
Okay, so how do we replace the MIME Multipart SIP body with a standard SDP?
#If the body is multipart then strip it and replace with a single part
if (has_body("multipart/mixed")) {
xlog("This has a multipart body");
if (filter_body("application/sdp")) {
remove_hf("Content-Type");
append_hf("Content-Type: application/sdp\r\n");
} else {
xlog("Body part application/sdp not found\n");
}
}
We’re doing more and more network automation, and something that came up as valuable to us would be to have all the IPs in HOMER SIP Capture come up as the hostnames of the VM running the service.
Luckily for us HOMER has an API for this ready to roll, and best of all, it’s Swagger based and easily documented (awesome!).
(Probably through my own failure to properly RTFM) I was struggling to work out the correct (current) way to Authenticate against the API service using a username and password.
Because the HOMER team are awesome however, the web UI for HOMER, is just an API client.
This means to look at how to log into the API, I just needed to fire up Wireshark, log into the Web UI via my browser and then flick through the packets for a real world example of how to do this.
In the Login action I could see the browser posts a JSON body with the username and password to /api/v3/auth
And in return the Homer API Server responds with a 201 Created an a auth token back:
Now in order to use the API we just need to include that token in our Authorization: header then we can hit all the API endpoints we want!
For me, the goal we were setting out to achieve was to setup the aliases from our automatically populated list of hosts. So using the info above I setup a simple Python script with Requests to achieve this:
import requests
s = requests.Session()
#Login and get Token
url = 'http://homer:9080/api/v3/auth'
json_data = {"username":"admin","password":"sipcapture"}
x = s.post(url, json = json_data)
print(x.content)
token = x.json()['token']
print("Token is: " + str(token))
#Add new Alias
alias_json = {
"alias": "Blog Example",
"captureID": "0",
"id": 0,
"ip": "1.2.3.4",
"mask": 32,
"port": 5060,
"status": True
}
x = s.post('http://homer:9080/api/v3/alias', json = alias_json, headers={'Authorization': 'Bearer ' + token})
print(x.status_code)
print(x.content)
#Print all Aliases
x = s.get('http://homer:9080/api/v3/alias', headers={'Authorization': 'Bearer ' + token})
print(x.json())
And bingo we’re done, a new alias defined.
We wrapped this up in a for loop for each of the hosts / subnets we use and hooked it into our build system and away we go!
With the Homer API the world is your oyster in terms of functionality, all the features of the Web UI are exposed on the API as the Web UI just uses the API (something I wish was more common!).
Using the Swagger based API docs you can see examples of how to achieve everything you need to, and if you ever get stuck, just fire up Wireshark and do it in the Homer WebUI for an example of how the bodies should look.
I recently fell in love with the Prometheus + Grafana combo, and I’m including it in as much of my workflow as possible, so today we’ll be integrating this with another favorite – Kamailio.
Why would we want to integrate Kamailio into Prometheus + Grafana? Observability, monitoring, alerting, cool dashboards to make it look like you’re doing complicated stuff, this duo have it all!
I’m going to assume some level of familiarity with Prometheus here, and at least a basic level of understanding of Kamailio (if you’ve never worked with Kamailio before, check out my Kamailio 101 Series, then jump back here).
So what will we achieve today?
We’ll start with the simple SIP Registrar in Kamailio from this post, and we’ll add on the xhttp_prom module, and use it to expose some stats on the rate of requests, and responses sent to those requests.
So to get started we’ll need to load some extra modules, xhttp_prom module requires xhttp (If you’d like to learn the basics of xhttp there’s also a Kamailio Bytes – xHTTP Module post covering the basics) so we’ll load both.
xHTTP also has some extra requirements to load, so in the top of our config we’ll explicitly specify what ports we want to bind to, and set two parameters that control how Kamailio handles HTTP requests (otherwise you’ll not get responses for HTTP GET requests).
Then where you load all your modules we’ll load xhttp and xhttp_prom, and set the basic parameters:
loadmodule "xhttp.so"
loadmodule "xhttp_prom.so"
# Define two counters and a gauge
modparam("xhttp_prom", "xhttp_prom_stats", "all")
By setting xhttp_prom module to expose all stats, this exposes all of Kamailio’s internal stats as counters to Prometheus – This means we don’t need to define all our own counters / histograms / gauges, instead we can use the built in ones from Kamailio. Of course we can define our own custom ones, but we’ll do that in our next post.
Lastly we’ll need to add an event route to handle HTTP requests to the /metrics URL:
This was a rebuild, another P-CSCF was running fine and handling traffic with the same DNS server set.
I checked the netplan config and confirmed the DNS server was set correctly.
If I did an nslookup on the address that was failing to resolve – pointing it at the correct DNS server, the A & SRV records came back OK, and everything was fine.
Stranger still, after clearing the DNS Cache, and running a packet capture, I couldn’t see any DNS queries at all….
The problem? Kamailio uses resolv.conf by default on Ubuntu Server, and that was pointing to localhost.
After updating resolv.conf to point to the DNS server handling the IMS domains, I was good to go again.
Recently I’ve been doing some work with FreeSWITCH as an IMS Conference Factory, I’ve written a bit about it before in this post on using FreeSWITCH with the AMR codec.
Pretty early on in my testing I faced a problem with subsequent in-dialog responses, like re-INVITEs used for holding the calls.
Every subsequent message, was getting a “420 Bad Extension” response from FreeSWITCH.
So what didn’t it like and why was FreeSWITCH generating 420 Bad Extension Responses to these subsequent messages?
Well, the “Extensions” FreeSWITCH is referring to are not extensions in the Telephony sense – as in related to the Dialplan, like an Extension Number to identify a user, but rather the Extensions (as in expansions) to the SIP Protocol introduced for IMS.
The re-INVITE contains a Require header with sec-agree which is a SIP Extension introduced for IMS, which FreeSWITCH does not have support for, and the re-INVITE says is required to support the call (Not true in this case).
Using a Kamailio based S-CSCF means it is easy to strip these Headers before forwarding the requests onto the Application Server, which is what I’ve done, and bingo, no more errors!
Recently I was working on a project that required Kamailio to constantly re-evaluate something, and generate a UAC request if the condition was met.
There’s a few use cases for this: For example you might want to get Kamailio to constantly check the number of SIP registrations and send an alert if they drop below a certain number. If a subscriber drops out in that their Registration just expires, there’s no SIP message that will come in to tell us, so we’d never be able to trigger something in the normal Kamailio request_route.
Of you might want to continually send a SIP MESSAGE to pop up on someone’s phone to drive them crazy. That’s what this example will focus on.
This is where the rtimer module comes in. You can define the check in a routing block, and then
Instead of going to all the effort of creating VMs (or running Ansible playbooks) we can use Docker and docker-compose to create a test environment with multiple Asterisk instances to dispatch traffic to from Kamailio.
I covered the basics of using Kamailio with Docker in this post, which runs a single Kamailio instance inside Docker with a provided config file, but in this post we’ll use docker-compose to run multiple Asterisk instances and setup Kamailio to dispatch traffic to them.
I am a big Kubernetes fan, and yes, all this can be done in Kubernetes, and would be a better fit for a production environment, but for a development environment it’s probably overkill.
#Copy the config file onto the Filesystem of the Docker instance
COPY dispatcher.list /etc/kamailio/
COPY kamailio.cfg /etc/kamailio/
The Kamailio config we’re using is very similar to the Dispatcher example but with a few minor changes to the timers and setting it to use the Dispatcher data from a text file instead of a database. If you have a look at the contents of dispatcher.list you’ll see three entries; dispatcher_w_docker_asterisk_1, dispatcher_w_docker_asterisk_2 & dispatcher_w_docker_asterisk_3. These will be the hostnames of the 3 Asterisk instances we’ll create.
Next up we’ll take a look at the docker-compose file, which defines how our environment will be composed, and defines which containers will be run
The docker-compose file contains definitions about the Containers we want to run, for this example we’ll run several Asterisk instances and a single Kamailio instance.
The replicas: 6 parameter is ignored by standard docker-compose up command, but will be used if you’re using Docker swarm, otherwise we’ll manually set the number of replicas when we run the command.
So with that defined let’s define our Kamailio service;
So far with most of our discussions about Kamailio we’ve talked about routing the initial SIP request (INVITE, REGISTER, SUBSCRIBE, etc), but SIP is not a one-message protocol, there’s a whole series of SIP messages that go into a SIP Dialog.
Sure the call may start with an INVITE, but there’s the 180 RINGING, the 200 OK and the ACK that go into getting the call actually established, and routing these in-dialog messages is just as important as routing the first INVITE.
When we’ve talked about SIP routing it’s all happened in the request_route {} block:
request_route {
xlog("Received $rm to $ru - Forwarding");
append_hf("X-Proxied: You betcha\r\n");
#Forward to new IP
forward("192.168.1.110");
}
In the example above we statelessly forward any initial requests to the IP 192.168.1.110.
We can add an onreply_route{} block to handle any replies from 192.168.1.110 back to the originator.
But why would we want to?
Some simple answers would be to do some kind of manipulation to the message – say to strip a Caller ID if CLIP is turned off, or to add a custom SIP header containing important information, etc.
onreply_route{
xlog("Got a reply $rs");
append_hf("X-Proxied: For the reply\r\n");
}
Let’s imagine a scenario where the destination our SIP proxy is relaying traffic to (192.168.1.110) starts responding with 404 error.
We could detect this in our onreply_route{} and do something about it.
onreply_route{
xlog("Got a reply $rs");
if($rs == 404) {
#If remote destination returns 404
xlog("Got a 404 for $rU");
#Do something about it
}
}
In the 404 example if we were using Dispatcher it’s got easily accessed logic to handle these scenarios a bit better than us writing it out here, but you get the idea.
There are a few other special routes like onreply_route{}, failure routes and event routes, etc.
Hopefully now you’ll have a better idea of how and when to use onreply_route{} in Kamailio.
It’s a seemingly simple question, the answer to which is – however you want, sorry if that’s not a simple answer.
I’ve talked about the strengths and weaknesses of Kamailio and Asterisk in my post Kamailio vs Asterisk, so how about we use them to work together?
The State of Play
So before we go into the nitty gritty, let’s imagine we’ve got an Asterisk box with a call queue with Alice and Bob in it, set to ring those users if they’re not already on a call.
Each time a call comes in, Asterisk looks at who in the queue is not already on a call, and rings their phone.
Now let’s imagine we’re facing a scenario where the single Asterisk box we’ve got is struggling, and we want to add a second to share the load.
We do it, and now we’ve got two Asterisk boxes and a Kamailio load balancer to split the traffic between the two boxes.
Now each time a call comes in, Kamailio sends the SIP INVITE to one of the two Asterisk boxes, and when it does, that Asterisk box looks at who is in the queue and not already on a call, and then rings their phone.
Let’s imagine a scenario where a Alice & Bob are both on calls on Asterisk box A, and another call comes in this call is routed to Asterisk box B. Asterisk box B looks at who is in the queue and who is already on a call, the problem is Alice and Bob are on calls on Asterisk box A, so Asterisk box B doesn’t know they’re both on a call and tries to ring them.
We have a problem.
Scaling stateful apps is a real headache,
So have a good long hard think about how to handle these issues before going down this path!
Sometimes you need Kamailio to serve as a User Agent Client, we covered using UAC to send SIP REGISTER messages and respond with the authentication info, but if you find you’re getting 401 or 407 responses back when sending an INVITE, you’ll need to use the UAC module, specifically the uac_auth() to authenticate the INVITE,
When Kamailio relays an INVITE to a destination, typically any replies / responses that are part of that dialog will go back to the originator using the Via headers.
This would be fine except if the originator doesn’t know the user name and password requested by the carrier, but Kamailio does,
Instead what we need Kamailio to do is if the response to the INVITE is a 401 Unauthorised Response, or a 407 Proxy Authentication required, intercept the request, generate the response to the authentication challenge, and send it to the carrier.
To do this we’ll need to use the UAC module in Kamailio and set some basic params:
Before we can call the t_relay() command, we need to specify a failure route, to be called if a negative response code comes back, we’ll use one called TRUNKAUTH and tell the transaction module that’s the one we’ll use by adding t_on_failure(“TRUNKAUTH”);
What we’ve done is specified to rewrite the destination URI to sip.nickvsnetworking.com, if the request type is an INVITE, it’ll load a failure route called TRUNKAUTH and proxy the request with the transaction module to sip.nickvsnetworking.com.
What we get is a 401 response back from our imaginary carrier, and included in it is a www-auth header for authentication.
To catch this we’ll create an on failure route named “TRUNKAUTH”
failure_route[TRUNKAUTH] {
xlog("trunk auth");
}
We’ll make sure the transaction hasn’t been cancelled, and if it has bail out (no point processing subsequent requests on a cancelled dialog).
failure_route[TRUNKAUTH] {
xlog("trunk auth");
if (t_is_canceled()) {
exit;
}
And determine if the response code is a 401 Unauthorised Response, or a 407 Proxy Authentication required (Authentication requests from our upstream carrier):
failure_route[TRUNKAUTH] {
xlog("trunk auth");
if (t_is_canceled()) {
exit;
}
xlog("Checking status code");
if(t_check_status("401|407")) {
xlog("status code is valid auth challenge");
}
}
Next we’ll define the username and password we want to call upon for this challenge, and generate an authentication response based on these values using the uac_auth() command,
failure_route[TRUNKAUTH] {
xlog("trunk auth");
if (t_is_canceled()) {
exit;
}
xlog("Checking status code");
if(t_check_status("401|407")) {
xlog("status code is valid auth challenge");
$avp(auser) = "test";
$avp(apass) = "test";
uac_auth();
}
}
Then finally we’ll relay that back to the carrier with our www-auth header populated with the challenge response;
I wrote about using Ansible to automate Kamailio config management, Ansible is great at managing VMs or bare metal deployments, but for Containers using Docker to build and manage the deployments is where it’s at.
I’m going to assume you’ve got Docker in place, if not there’s heaps of info online about getting started with Docker.
The Dockerfile
The Kamailio team publish a Docker image for use, there’s no master branch at the moment, so you’ve got to specify the version; in this case kamailio:5.3.3-stretch.
Once we’ve got that we can start on the Dockerfile,
For this example I’m going to include
#Kamailio Test Stuff
FROM kamailio/kamailio:5.3.3-stretch
#Copy the config file onto the Filesystem of the Docker instance
COPY kamailio.cfg /etc/kamailio/
#Print out the current IP Address info
RUN ip add
#Expose port 5060 (SIP) for TCP and UDP
EXPOSE 5060
EXPOSE 5060/udp
Once the dockerfile is created we can build an image,
docker image build -t kamtest:0.1 .
And then run it,
docker run kamtest:0.1
Boom, now Kamailio is running, with the config file I pushed to it from my Dockerfile directory,
Now I can setup a Softphone on my local machine and point it to the IP of the Docker instance and away we go,
Where the real power here comes in is that I can run that docker run command another 10 times, and have another 10 Kamailio instannces running.
Tie this in with Kubernetes or a similar platform and you’ve got a way to scale and manage upgrades unlike anything you’d get on Bare Metal or VMs.
I had a few headaches getting the example P-CSCF example configs from the Kamailio team to run, recent improvements with the IPsec support and code evolution meant that the example config just didn’t run.
So, after finally working out the changes I needed to make to get Kamailio to function as a P-CSCF, I took the plunge and made my first pull request on the Kamailio project.
Today I thought I’d cover a simple use case – running an HTTP get from Kamailio and doing something with the output.
The http_client does what it sounds – Acts as an HTTP client to send HTTP GET and POST requests.
The use cases for this become clear quite quickly, you could use http_client to request credit from an accounting server via it’s API, get the latest rate to a destination from a supplier, pull weather data, etc, etc.
Let’s take a very simple example, we’ll load http_client by adding a loadmodule line:
...
loadmodule "http_client.so"
...
Next I’ve put together a very simple request_route block that requests a HTTP file from a web server and sends the response to a SIP client:
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
xlog("Got request");
http_client_query("https://nickvsnetworking.com/utils/curl.html", "", "$var(result)");
xlog("Result is $var(result)");
sl_reply("200", "Result from HTTP server was $var(result)");
}
Using the http_client_query() function we’re able to query a HTTP server,
We’ll query the URL https://nickvsnetworking.com/utils/curl.html and store the output to a variable called result.
If you visit the URL you’ll just get a message that says “Hello there“, that’s what Kamailio will get when it runs the http_client function.
Next we print the result using an xlog() function so we can see the result in syslog,
Finally we send a stateless reply with the result code 200 and the body set to the result we got back from the server.
We can make this a bit more advanced, using HTTP Post we can send user variables and get back responses.
The http_client module is based on the ubiquitous cURL tool, that many users will already be familiar with.
It’s probably pretty evident to most why you’d want to use TLS these days,
SIP Secure – aka sips has been around for a long time and is supported by most SIP endpoints now.
Kamailio supports TLS, and it’s setup is very straightforward.
I’ve got a private key and certificate file for the domain nickvsnetworking.com so I’ll use those to secure my Kamailio instance by using TLS.
I’ll start by copying both the certificate (in my case it’s cert.pem) and the private key (privkey.pem) into the Kamailio directory. (If you’ve already got the private key and certificate on your server for another application – say a web server, you can just reference that location so long as the permissions are in place for Kamailio to access)
Next up I’ll open my Kamailio config (kamailio.cfg), I’ll be working with an existing config and just adding the TLS capabilities, so I’ll add this like to the config:
!define WITH_TLS
That’s pretty much the end of the configuration in kamailio.cfg, if we have a look at what’s in place we can see that the TLS module loads it’s parameters from a separate file;
After restarting Kamailio subscribers can now contact us via TLS using sips.
You may wish to disable TCP & UDP transport in favor of only TLS.
A note about CAs…
If you’re planning on rolling out SIP over TLS (sips) to existing IP phones it’s worth looking at what Certificate Authorities (CAs) are recognised by the IP phones.
As TLS relies on a trust model where a CA acts kind of like a guarantor to the validity of the certificate, if the IP phone doesn’t recognise the CA, it may see the certificate as Invalid.
Some IP phones for example won’t recognize Let’s Encrypt certificates as valid, while others may not recognize any of the newer CAs.
If you’re running a Debian system, the Kamailio team provide nightly development builds as Debian packages that can be installed on Debian or Ubuntu systems using the apt package manager.
Installing is a breeze, first we just add the GPG key for the repo:
Despite the fact it’s 2020 there’s still a lot of folks in the world manually configuring boxes,
Ansible is a topic I could talk all day about, but in essence it’s kind of an automation framework, tell Ansible what to do one and it can spin you up two boxes, or two thousand boxes and manage the config on them.
I talked about DMQ, the Distributed Message Queue in a Kamailio Bytes post some time ago, and so as an example I’ll share an example playbook to Install Kamailio the lazy way from the Repos, and load the DMQ config with the IP Address and DMQ Address pulled from variables based on the host itself.
There’s a huge number of posts on installing and the basics of Ansible online, if you’re not familiar with Ansible already I’d suggest starting by learning the basics and then rejoining us.
The Hosts
Depending on if your hosts are running on bare metal, VMWare VMs or cloud based, I’m going to assume you’re working with a Debian system.
I’ve already got 3 servers ready to go, they’ve got sequential IP Addresses so I’ve added the range to my /etc/ansible/hosts file:
I’ve created the group kamailio and put the IP Address range 10.0.1.193 to 10.0.1.195 in there.
You will probably need to add the authentication info, such as passwords, private keys and privilege escalation details, but I’m going to assume you’ve already done that and you can run the ping module on each one:
ansible kamailio -m ping
Assuming that comes back OK and you can get into each one let’s move onto the Playbook.
The Playbook
There’s a few tasks we’ll get out of the way before we configure Kamailio,
The first of which is adding the Debian repo and the keys,
Next we’ll load a Kamailio config from a template that fills in our IP Address and Kamailio version, then we’ll install Kamailio,
Rather than talk you through each of the plays here’s a copy of my playbook:
---
- name: Configure Kamailio
hosts: kamailio
become: yes
vars:
kamailio_version: "53"
debian_sources_dir: "/etc/apt/sources.list.d"
tasks:
- name: Add keys for Kamailio repo
apt_key:
url: http://deb.kamailio.org/kamailiodebkey.gpg
state: present
- name: Add repo to sources.list
apt_repository:
repo: deb http://deb.kamailio.org/kamailio{{kamailio_version}} {{hostvars[inventory_hostname]['ansible_lsb']['codename']}} main
#The full list of Debian repos can be found at http://deb.kamailio.org/
#The version is based off the versions listed there and the release is based on the codename of the Debian / Ubuntu release.
state: present
- name: Copy Config Template
#Copies config from the template, fills in variables and uplaods to the server
template:
src: kamailio.cfg.j2
dest: /etc/kamailio/kamailio.cfg
owner: root
group: root
backup: yes
register: config_changed
- name: Install Kamailio
#Updates cache (apt-get update) and then installs Kamailio
apt:
name: kamailio
update_cache: yes
state: present
register: kamailio_installed_firstrun
- name: Restart Kamailio if config changed
service:
name: kamailio
state: restarted
when: config_changed.changed
- name: Start Kamailio if installed for the first time
service:
name: kamailio
state: started
when: kamailio_installed_firstrun.changed
Should be pretty straight forward to anyone who’s used Ansible before, but the real magic happens in the templatemodule. Let’s take a look;
Kamailio config in Jinja2 template
Pusing out static config is one thing, but things like IP Addresses, FQDNs and SSL certs may differ from machine to machine, so instead of just pushing one config, I’ve created a config and added some variables in Jinja2 format to the config, that will be filled with the value on the target when pushed out.
In the templatemodule of the playbook you can see I’ve specified the file kamailio.cfg.j2 this is just a regular Kamailio config file but I’ve added some variables, let’s look at how that’s done.
On the machine 10.0.1.194 we want it to listen on 10.0.1.194, we could put list 0.0.0.0 but this can lead to security concerns, so instead let’s specify the IP in the Jinja config,
By putting ansible_default_ipv4.address in two sets of curly brackets, this tells Ansible to fill in thes values from the template with the Ansible IPv4 Address of the target machine.
Let’s take a look on the 10.0.1.194’s actual kamailio.cfg file:
Let’s take another example,
To keep DMQ humming it makes sense to have different DMQ domains for different versions of Kamailio, so in the Kamailio config file template I’ve called the variable kamailio_versionin the DMQ address,
This means on a Kamailio 5.2 version this URL look like this on the boxes’ config:
Running it is just a simple matter of calling ansible-playbook and pointing it at the playbook we created, here’s how it looks setting up the 3 hosts from their vanilla state:
The great thing about Kamailio is it’s omnipotent – This means it will detect if it needs to do each of the tasks specified in the playbook.
So if we run this again it won’t try and add the repo, GPG keys, install Kamailio and load the template, it’ll look and see each of those steps have already been done and skip each of them.
But what if someone makes some local changes on one of the boxes, let’s look at what happens:
Likewise now if we decide to change our config we only need to update the template file and Ansible will push it out to all our machines, I’ve added a comment into the Kamailio template, so let’s run it again and see the config pushed out to all the Kamailio instances and them restarting.
In my last post I talked about using KEMI in Kamailio and how you can integrate in a different programming language to handle your SIP request handling in a language you already know – Like Python!
Before we get too excited there’s some boilerplate we’ve got to add to our Python script, we need to create a class called kamailio and populate the class by defining some functions, we’ll define an __init__ to handle loading of the class, define a child_init for handling child processes, define ksr_request_route to handle the initial requests. We’ll also need to define a mod_init – outside of the Kamailio class to initialize the class.
import sys
import Router.Logger as Logger
import KSR as KSR
import requests
# global function to instantiate a kamailio class object
# -- executed when kamailio app_python module is initialized
def mod_init():
KSR.info("===== from Python mod init\n");
return kamailio();
# -- {start defining kamailio class}
class kamailio:
def __init__(self):
KSR.info('===== kamailio.__init__\n');
# executed when kamailio child processes are initialized
def child_init(self, rank):
KSR.info('===== kamailio.child_init(%d)\n' % rank);
return 0;
# SIP request routing
# -- equivalent of request_route{}
def ksr_request_route(self, msg):
KSR.info("===== request - from kamailio python script\n");
KSR.dbg("method " + KSR.pv.get("$rm") + " r-uri " + KSR.pv.get("$ru"))
Most of these should be pretty self explanatory for anyone who’s done a bit more in-depth Python programming, but it’s no big deal if you don’t understand all this, the only part you need to understand is the ksr_request_route function.
ksr_request_route: translates to our request_route{} in the Kamailio native scripting language, all requests that come in will start off in this part.
Python Kamailio Routing
So let’s start to build upon this, so we’ll blindly accept all SIP registrations;
Here you’ll see we’ve added an if statement, as if we were doing any other If statement in Python, in this case we’re asking if the KSR.is_method(“REGISTER”), and if it is, we’ll send back a 200 OK response.
Let’s pause and talk about KSR
All the Kamailio bits we’ll use in Python will have the KSR. prefix, so let’s take a quick break here to talk about KSR. The KSR. functions are the KEMI functions we’ve exposed to Python.
Without them, we’re just writing Python, and we’d have to do all the functions provided by Kamailio nativeley in Python, which would be crazy.
So we leverage the Kamailio modules you know and love from Python using Python’s logic / programming syntax, as well as opening up the ability to pull in other libraries from Python.
Let’s look at how we might send a stateless reply,
There’s a module function to send a stateless reply;
KSR.sl.send_reply(200, "OK")
The vast majority of functions are abstracted as module functions, like the example above, but not all of them.
So every function doesn’t need to be wrapped up as a module, there’s also a way to call any function that you’d call from the native scripting language, wrapped up, kind of like an Exec command:
KSR.x.modf("sl_send_reply", "200", "OK");
So thanks to this we can call any Kamailio function from Python, even if it’s not explicitly in the KEMI abstraction.
Python Kamailio Routing (Continued)
So earlier we managed REGISTER requests and sent back a 200 OK response.
What about forwarding a SIP Request to another proxy? Let’s follow on with an elif statement to test if the method is an INVITE and statelessly forward it.
elif KSR.is_method("INVITE"):
#Lookup our public IP address
try:
ip = requests.get('https://api.ipify.org').text
except:
ip = "Failed to resolve"
#Add that as a header
KSR.hdr.append("X-KEMI: I came from KEMI at " + str(ip) + "\r\n");
#Set host IP to 10.1.1.1
KSR.sethost("10.1.1.1");
#Forward the request on
KSR.forward()
Now an incoming SIP invite will be proxied / forwarded to 10.1.1.1, all from Python.
But so far we’ve only done things in KEMI / Python that we could do in our native Kamailio scripting language, so let’s use some Python in our Python!
I utterly love the Python Requests library, so let’s use that to look up our public IP address and add it as a header to our forwarded SIP INVITE;
elif KSR.is_method("INVITE"):
#Lookup our public IP address
try:
ip = requests.get('https://api.ipify.org').text
except:
ip = "Failed to resolve"
#Add that as a header
KSR.hdr.append("X-KEMI: I came from KEMI at " + str(ip) + "\r\n");
#Set host IP to 10.1.1.1
KSR.sethost("10.1.1.1");
#Forward the request on
KSR.forward()
Bottom header is the X-KEMI custom header we included with our public IP
So let’s wrap this up a bit and handle any other request that’s not an INVITE or a REGISTER, with a 500 error code.
# SIP request routing
# -- equivalent of request_route{}
def ksr_request_route(self, msg):
KSR.dbg("method " + KSR.pv.get("$rm") + " r-uri " + KSR.pv.get("$ru"))
if KSR.is_method("REGISTER"):
KSR.sl.send_reply(200, "OK")
elif KSR.is_method("INVITE"):
#Lookup our public IP address
try:
ip = requests.get('https://api.ipify.org').text
except:
ip = "Failed to resolve"
#Add that as a header
KSR.hdr.append("X-KEMI: I came from KEMI at " + str(ip) + "\r\n");
#Set host IP to 10.1.1.1
KSR.sethost("10.1.1.1");
#Forward the request on
KSR.forward()
else:
KSR.sl.send_reply(500, "Got no idea...")