Kamailio en tant que serveur professionnel SIP vient avec son propre « langage de programmation » (ou plus précisément un langage de description). Celui-ci dispose d’une syntaxe et d’une structure qui lui sont propres. Quoique similaire dans un certain sens à des langages de script comme du Shell ou du JavaScript, le langage dédié à Kamailio demande d’être appris et compris pour arriver à ses fins.
Dans ce deuxième article et tutoriel dédié à la découverte de Kamailio, je vais tenter d’expliquer les notions afférentes à la programmation de cet outil. Nous étudierons la syntaxe de base, les pseudo-variables, les routes et blocs ainsi que la structure du fichier de configuration par défaut. Pour terminer, je vous proposerai un exemple de configuration minimale pour répondre à un UAC.
Si vous n’avez pas encore lu la première partie de ce tutoriel, je vous invite à consulter l’article d’introduction à Kamailio.
Programmer Kamailio : le langage de configuration
Syntaxe de base
Commentaires
Les commentaires sur une ligne commencent par # ou //. Les commentaires sur plusieurs lignes se présentent comme en C/C++ :
# Ceci est un commentaire sur une ligne
// Ceci est aussi un commentaire sur une ligne
/* Ceci est un commentaire
* sur plusieurs lignes
*/
Attention : les lignes commençant par #! ne sont pas des commentaires. Ce sont des directives du préprocesseur, que nous allons voir juste après.
Directives du préprocesseur
Les directives du préprocesseur permettent d’influencer le comportement général de Kamailio sans avoir à en modifier la logique applicative. On peut par exemple décider de charger un module, d’activer le mode debug ou encore d’activer le support les terminaux derrière un réseau avec NAT (Network Address Translation) – c’est d’ailleurs très souvent avec une directive préprocesseur q’uon peut constater l’activation du NAT dans le scripts sur GitHub.
Ces directives sont évaluées avant l’interprétation du fichier de configuration, exactement comme avec un compilateur C/C++.
Les directives les plus courantes sont :
#!define permet de déclarer une constante ou une variable globale. #!ifdef, #!ifndef, #!else et #!endif forment la structure conditionnelle. #!include_file permet d’inclure un fichier externe. #!subst et #!substdef effectuent des substitutions de chaînes de caractères.
Voici un exemple tiré de la configuration par défaut de Kamailio :
#!ifdef WITH_DEBUG
#!define DBGLEVEL 3
#!else
#!define DBGLEVEL 2
#!endif
Si la variable globale WITH_DEBUG est définie, alors DBGLEVEL vaut 3, sinon elle vaut 2. Dans la pratique, on active ces variables globales soit en les ajoutant en haut du fichier de configuration (#!define WITH_DEBUG), soit en les passant en paramètre au démarrage de Kamailio (kamailio -A WITH_DEBUG).
La philosophie du fichier de configuration par défaut repose largement sur ce mécanisme : les fonctionnalités comme la gestion du NAT, l’authentification ou le support MySQL sont activables via des directives WITH_NAT, WITH_AUTH, WITH_MYSQL, etc. Cela permet de garder un fichier unique tout en activant ou désactivant des blocs entiers de configuration. Dans le cas d’une exécution dans un container Docker par exemple, les directives préprocesseur permettent de rendre Kamailio « dynamique » en lui passant par exemple des variables d’environnement, ce qu’il ne supporte pas nativement. On peut pour cela utiliser la directive d’inclusion de fichier qui suit.
L’inclusion de fichiers externes est également très utile pour séparer la configuration en plusieurs fichiers :
#!include_file "kamailio-local.cfg"
Cette ligne inclut le contenu du fichier kamailio-local.cfg avant l’interprétation. Cela permet par exemple de surcharger des variables ou d’activer des routes sans toucher au fichier principal.
Les paramètres globaux (core settings)
Avant les blocs de routage, le fichier de configuration définit les paramètres globaux qui contrôlent le comportement du serveur. Ces paramètres se placent en haut du fichier, après les directives préprocesseur.
#!define DBGLEVEL 2
debug=DBGLEVEL # Niveau de log (0 = minimal, 3 = verbose)
log_stderror=no # Logs vers syslog, pas stderr
fork=yes # Kamailio tourne en mode daemon (fork)
children=4 # Nombre de processus enfants pour traiter les requêtes
listen=udp:0.0.0.0:5060 # Écouter sur toutes les interfaces en UDP, port 5060
listen=tcp:0.0.0.0:5060 # Idem en TCP
listen=udp:[::]:5060 # Écouter UDP IPv6, port 5060
listen=tcp:[::]:5060 # Idem en TCP
alias="voip.example.com" # Domaine considéré comme local par Kamailio
# mpath="/usr/lib/x86_64-linux-gnu/kamailio/modules" # Chemin vers les modules. Ici on a commenté la ligne car on laisse Kamailio utilisé le chemin par défaut.
Le paramètre listen mérite une attention particulière : il définit les interfaces et protocoles sur lesquels Kamailio écoute les messages SIP. On peut le déclarer plusieurs fois pour écouter sur plusieurs interfaces ou protocoles. Ici on écoute sur toutes les interfaces en IPv4 et en IPv6, en TCP et UDP et systématiquement sur le port 5060. Même si ce n’est pas le sujet de cet article dans le cas du TLS, il faut écouter sur le port 5061 et déclarer les certificats à utiliser.
Le paramètre alias indique à Kamailio quels domaines il doit considérer comme les siens, ce qui est important pour déterminer si une requête est destinée au serveur lui-même ou doit être relayée. On peut imaginer un opérateur déclarer son domaine d’endpoint SIP comme alias, par exemple : sip.operateur.re.
Le paramètre children définit le nombre de processus créés par Kamailio pour traiter les requêtes SIP. En production, cette valeur est généralement augmentée en fonction de la charge attendue ou du nombre de CPU disponibles.
Charger des modules
Kamailio repose sur une architecture modulaire. Le cœur du logiciel (core) fournit les fonctionnalités de base pour parser et router les messages SIP, mais la plupart des fonctionnalités utiles proviennent de modules qu’il faut connaitre et charger explicitement. Pour rappel à cet effet, la liste des modules est disponible sur la documentation officielle.
Le chargement d’un module se fait avec l’instruction loadmodule et sa configuration avec modparam :
# Chargement des modules
loadmodule "sl.so" # Réponses stateless (sans transaction)
loadmodule "tm.so" # Gestion des transactions SIP
loadmodule "pv.so" # Pseudo-variables
loadmodule "xlog.so" # Logging avancé avec pseudo-variables
loadmodule "textops.so" # Fonctions de manipulation de texte SIP
loadmodule "siputils.so" # Utilitaires SIP divers
# Configuration des modules
modparam("sl", "bind_tm", 0) # Paramétrage du module sl
Parmi les modules qu’on utilise le plus souvent dans une configuration basique : sl permet d’envoyer des réponses SIP sans créer de transaction (mode stateless), tm gère les transactions SIP (mode stateful), pv rend disponibles les pseudo-variables, et xlog permet d’écrire des messages de log qui contiennent des pseudo-variables (l’IP source, la méthode SIP, etc.).
Note : je vous invite par bonne pratique dans une pile de production à généraliser les logs avec xlog afin de diagnostiquer si nécessaire d’éventuelles erreurs de configuration ou des comportements anormaux.
Variables et pseudo-variables
Les pseudo-variables sont le concept central du langage de configuration de Kamailio. Elles donnent accès aux champs du message SIP en cours de traitement, aux informations sur la connexion réseau, et à des espaces de stockage temporaires. Elles commencent toujours par le caractère $.
Voici quelques pseudo-variables à connaître pour débuter avec Kamailio :
| Pseudo-variable | Description | Exemple de valeur |
|---|---|---|
$rm | Méthode de la requête SIP | INVITE, REGISTER, OPTIONS |
$ru | Request-URI complet | sip:1002@voip.example.com |
$rU | Partie utilisateur du Request-URI | 1002 |
$rd | Domaine du Request-URI | voip.example.com |
$si | Adresse IP source du message | 192.168.1.50 |
$sp | Port source du message | 5060 |
$fu | From URI | sip:1001@voip.example.com |
$tu | To URI | sip:1002@voip.example.com |
$ci | Call-ID du message | abc123@192.168.1.50 |
$hdr(X) | Valeur du header SIP X | $hdr(User-Agent) |
On utilise ces pseudo-variables dans les conditions, les logs et les fonctions de manipulation. Par exemple :
if ($rm == "INVITE") {
xlog("L_INFO", "Appel entrant de $fu vers $rU via $si:$sp\n");
}
En plus des pseudo-variables de lecture du message SIP, Kamailio propose plusieurs types de variables de stockage utilisables dans le script :
$var(x)désigne une variable de script, locale au processus en cours. Attention : elle persiste entre les requêtes traitées par le même processus, il est donc nécessaire de toujours l’initialiser avant utilisation.$avp(x)est une Attribute-Value Pair, attachée à la transaction SIP en cours, qui est automatiquement détruite à la fin du traitement.$shv(x)est une variable partagée en mémoire, visible par tous les processus Kamailio — utile par exemple pour un compteur global ou un interrupteur de debug.
Nous reviendrons plus en détail sur ces types de variables dans les prochains articles, au fur et à mesure des cas d’usage.
Routes et blocs : la structure d’un programme Kamailio
Le point d’entrée : request_route
Chaque requête SIP reçue par Kamailio passe par le bloc request_route (ou parfois simplement abrégé route). C’est l’équivalent de la fonction main() en C ou du point d’entrée dans n’importe quel programme. Toute la logique de routage commence ici.
request_route {
# Chaque requête SIP reçue par Kamailio entre ici.
# C'est à nous de décider quoi en faire.
xlog("L_INFO", "Requête $rm reçue de $si:$sp\n");
}
Si request_route ne contient aucune instruction qui transfert ou répond à la requête, Kamailio abandonnera silencieusement le message. C’est ce comportement qui fait que Kamailio « ne fait rien » par défaut. À nous donc de le programmer pour en tirer quelque chose de viable – surtout si on envisage de faire passer des communications critiques dessus.
Fonction : les routes nommées
Les routes nommées sont l’équivalent des fonctions dans un langage de programmation classique. Elles permettent de découper la logique en blocs réutilisables et lisibles. On les déclare avec route[NOM] et on les appelle avec route(NOM) – notez la subtilité ici qui peut vous valoir des heures de debug.
request_route {
# Point d'entrée
route(REQINIT);
if ($rm == "OPTIONS") {
route(HANDLE_OPTIONS);
}
}
route[REQINIT] {
# Vérifications initiales sur chaque requête
if ($si == "192.168.1.100") {
xlog("L_WARN", "Requête de l'IP de test\n");
}
}
route[HANDLE_OPTIONS] {
# Répondre 200 OK aux requêtes OPTIONS
sl_send_reply("200", "OK");
exit;
}
La configuration par défaut de Kamailio utilise abondamment les routes nommées pour organiser sa logique : REQINIT pour les vérifications initiales, WITHINDLG (littéralement « PENDANT un DIALOGUE » en français) pour les requêtes à l’intérieur d’un dialogue existant, REGISTRAR pour l’enregistrement, LOCATION pour la localisation de l’appelé, RELAY pour relayer des messages, etc.
Les routes nommées peuvent retourner une valeur entière avec return(n). La valeur de retour est accessible via la pseudo-variable $rc (ou $retcode) :
route[CHECK_SOURCE] {
if ($si == "10.0.0.1") {
return(1); # Source autorisée
}
return(-1); # Source refusée
}
request_route {
route(CHECK_SOURCE);
if ($rc < 0) {
sl_send_reply("403", "Forbidden");
exit;
}
}
Les routes spécialisées
En plus de request_route et des routes nommées, Kamailio dispose de plusieurs types de routes spécialisées qui s’exécutent à des moments précis du cycle de vie d’une transaction SIP.
branch_route[NAME] est exécutée pour chaque branche d’un fork. Un fork se produit lorsqu’un utilisateur est enregistré avec plusieurs terminaux (par exemple un téléphone de bureau et un softphone) : Kamailio envoie la requête vers chaque contact enregistré, et la branch_route permet d’agir sur chaque branche individuellement.
onreply_route[NAME] permet de traiter les réponses SIP (1xx, 2xx, 3xx, etc.) associées à une transaction. C’est ici qu’on peut par exemple modifier des headers dans une réponse ou appliquer du traitement NAT sur les réponses. Cette route permet par exemple de lire la réponse venant d’un Asterisk ou d’un FreeSwitch placé en aval de la requête pour utiliser Kamailio en tant que mid-registrar et enregistrer la location de l’UAC.
failure_route[NAME] est déclenchée lorsqu’une transaction échoue, c’est-à-dire lorsqu’une réponse négative finale est reçue (4xx, 5xx, 6xx). C’est le bloc idéal pour implémenter un basculement (failover) : si le premier destinataire ne répond pas, on peut envoyer l’appel vers un autre.
reply_route (sans nom) est la route globale pour toutes les réponses SIP reçues. Dans la pratique, on utilise plutôt les onreply_route nommées associées à des transactions spécifiques.
event_route[...] regroupe des routes déclenchées par des événements internes de Kamailio ou de ses modules. Par exemple, event_route[tm:local-request] est exécutée pour les requêtes générées localement par le module tm.
Nous verrons ces routes spécialisées en pratique dans les prochains articles, lorsque notre configuration le nécessitera.
Flux d’exécution
Pour programmer Kamailio il va vous falloir comprendre le flux d’exécution. Voici un petit point sur les modes possibles de traitement d’une requête SIP : stateless ou stateful. En mode stateless, Kamailio ne fait que traiter et transférer la requête et n’assure aucun suivi de celle-ci. En mode stateful Kamailio garde en mémoire la « trace » ce qui débloque certaines routes.
Comprendre le flux d’exécution est essentiel pour programmer Kamailio efficacement. Lorsqu’une requête SIP arrive, voici le parcours qu’elle suit :
La requête entre dans request_route. Si la logique applicative décide de relayer la requête en mode stateful (avec le module tm et la fonction t_relay()), Kamailio crée une transaction. Avant l’envoi de chaque branche, la branch_route correspondante est exécutée si elle a été définie. Les réponses du destinataire passent par la onreply_route. En cas d’échec, c’est la failure_route qui est déclenchée, ce qui permet de tenter un autre destinataire ou d’effectuer des actions sur cette réponse.
Trois instructions de contrôle sont importantes à connaître dans ce contexte : exit arrête l’exécution du script pour la requête en cours (la requête n’est pas détruite si elle est dans une transaction). drop arrête l’exécution et détruit la requête, elle ne sera ni relayée ni répondue. return(n) quitte la route en cours et revient au bloc appelant avec un code de retour.
Il est important de distinguer le mode stateless du mode stateful. En mode stateless, Kamailio traite chaque message SIP de manière indépendante avec les fonctions du module sl (par exemple sl_send_reply()). En mode stateful, Kamailio crée une transaction avec le module tm et peut donc associer requêtes et réponses, ce qui ouvre la porte aux failure_route, onreply_route et au failover.
kamailio.cfg : structure et philosophie
Par défaut, Kamailio est livré avec un fichier de configuration complet disponible à l’emplacement /etc/kamailio/kamailio.cfg. Celui-ci fait plus de 1 100 lignes et peut sembler intimidant au premier abord. Vous pouvez le retrouver dans le dépôt officiel sur GitHub.
Ce fichier est organisé en quatre grandes sections qui suivent toujours le même ordre.
La première section contient les directives et définitions : l’entête avec les commentaires d’aide, les directives #!define WITH_* pour activer les fonctionnalités, et l’import du fichier kamailio-local.cfg. C’est aussi ici qu’on retrouve l’explication des variables globales disponibles et de leur effet.
La deuxième section regroupe les paramètres globaux : le niveau de debug, les interfaces d’écoute (listen), les alias de domaine et les chemins vers les modules.
La troisième section est le chargement et la configuration des modules : chaque loadmodule est accompagné de ses modparam. Les modules chargés dépendent des directives activées dans la première section, grâce aux blocs #!ifdef.
La quatrième et dernière section contient les blocs de routage : request_route comme point d’entrée, puis les routes nommées (REQINIT, WITHINDLG, REGISTRAR, LOCATION, RELAY, etc.) ainsi que les routes spécialisées.
Ce fichier est un excellent point de référence et c’est une bonne pratique de le lire en entier au moins une fois. Cependant, pour apprendre Kamailio, il est plus efficace de partir d’une configuration minimale que l’on enrichit progressivement — c’est ce que nous allons faire maintenant.
Exemple de configuration minimale fonctionnelle
Voici une configuration minimale qui fait quelque chose de concret : elle répond 200 OK aux requêtes OPTIONS (souvent utilisées comme health check par les équipements SIP), elle logge toutes les requêtes reçues, et elle répond 501 Not Implemented à tout le reste.
#!KAMAILIO
#
# Configuration minimale de démonstration
# Kamailio tutoriel partie 2
#
## === Paramètres globaux ===
debug=2
log_stderror=yes # Pratique pour le développement, mettre "no" en production
fork=yes
children=2
listen=udp:0.0.0.0:5060
## === Chemin des modules ===
mpath="/usr/lib/x86_64-linux-gnu/kamailio/modules/"
## === Chargement des modules ===
loadmodule "sl.so" # Réponses stateless
loadmodule "pv.so" # Pseudo-variables
loadmodule "xlog.so" # Logging avancé
loadmodule "textops.so" # Manipulation de texte SIP
loadmodule "siputils.so" # Utilitaires SIP
## === Bloc de routage principal ===
request_route {
# Logger chaque requête reçue
xlog("L_INFO", "Requête $rm reçue de $si:$sp - R-URI: $ru\n");
# Répondre 200 OK aux requêtes OPTIONS
if (is_method("OPTIONS")) {
sl_send_reply("200", "OK");
exit;
}
# Pour tout le reste, répondre 501 Not Implemented
xlog("L_NOTICE", "Méthode $rm non gérée, réponse 501\n");
sl_send_reply("501", "Not Implemented");
exit;
}
Cette configuration tient en une quarantaine de lignes et utilise uniquement le mode stateless (module sl). Elle ne crée aucune transaction, ne gère pas l’enregistrement des utilisateurs et ne relaie aucun appel. Son objectif est simplement de montrer une configuration qui réagit aux messages SIP.
Pour tester cette configuration, commencez par sauvegarder le fichier de configuration par défaut puis remplacez-le par celui ci-dessus :
$ sudo cp /etc/kamailio/kamailio.cfg /etc/kamailio/kamailio.cfg.bak
$ sudo nano /etc/kamailio/kamailio.cfg # Coller la configuration ci-dessus
Vérifiez que la configuration est valide avant de (re)démarrer Kamailio :
$ sudo kamailio -c # Vérifie la syntaxe du fichier de configuration
$ sudo systemctl restart kamailio
Vous pouvez ensuite envoyer une requête OPTIONS avec l’utilitaire sipsak pour vérifier que Kamailio répond correctement :
$ sudo apt install sipsak
$ sipsak -s sip:test@127.0.0.1:5060
## Résultat attendu : SIP/2.0 200 OK
Vous pouvez aussi utiliser un véritable UAC (comme MicroSIP sur Windows, Telephone sur MacOS ou encore Calls sur GNU/Linux). Vous noterez ici que votre softphone ne pourra rien faire ni même passer des appels car son enregistrement échoue systématiquement du fait de la réponse 501 retournée par Kamailio.
Si vous souhaitez observer le trafic SIP en temps réel, installez sngrep, un outil en ligne de commande qui capture et affiche les messages SIP de manière lisible :
$ sudo apt install sngrep
$ sudo sngrep
Vous verrez alors votre requête OPTIONS et la réponse 200 OK envoyée par Kamailio. Les logs de Kamailio (via journalctl -u kamailio ou stderr si log_stderror=yes) afficheront les messages xlog que nous avons définis.
Pour continuer
Dans le prochain chapitre, nous verrons comment configurer Kamailio pour en faire un registrar. Celui-ci acceptera les requêtes REGISTER des UAC que nous provisionnerons, et nous pourrons dès le chapitre 4 effectuer des appels entre utilisateurs enregistrés. J’ajoute également le fait que nous étudierons également une solution de protection de PBX basée sur Kamailio sur le blog. Etant donné que nous avons toutes les bases pour faire du stateless nous pouvons d’ores et déjà faire une solution de « pare-feu » SIP.