Infra VOIP : On monte notre PBX multitenant

Quelle solution ?

Je reste dans la même politique d'opensource et si possible français. Il faut que le PBX soit multitenant (où un tenant = un client). Dans le domaine de la téléphonie, presque la totalité des solutions sont basés sur asterisk soit freeswitch qui sont tous les deux .... opensources !

Vous pouvez donc deviner le nombre gigantesque de solution qui répond donc à mes critères. Je vais donc partir sur Wazo (fork de Xivo). Grosso modo Wazo propose des API (developpées en python) qui vont taper un asterisk derrière. Il y a aussi une interface web ce qui peut être pratique. Allons donc l'installer !

Installation

Pour commencer, on installe quelques paquets et on clone le projet github :

naradmin@wazo:~$ sudo apt install sudo git ansible curl -yq
naradmin@wazo~$ git clone https://github.com/wazo-platform/wazo-ansible.git

Ensuite, on définit la version de wazo à installer (j'ai choisi la stable, plutôt logique mais par défaut c'est la version dev) puis on installe la bdd via pip :

naradmin@wazo:~$ cd wazo-ansible/
naradmin@wazo:~$ ansible_tag=wazo-$(curl https://mirror.wazo.community/version/stable)
naradmin@wazo:~/wazo-ansible$ git checkout $ansible_tag
naradmin@wazo:~/wazo-ansible$ ansible-galaxy install -r requirements-postgresql.yml

Avant d'installer, il faut définir certaines variables dans inventories/uc-engine

[all:vars]
ansible_python_interpreter = /usr/bin/python3

[uc_engine_host]
localhost ansible_connection=local

[database:children]
uc_engine_host

[engine_api:children]
uc_engine_host

[b2bua:children]
uc_engine_host

[uc_engine:children]
b2bua
database
engine_api

[uc_ui:children]
uc_engine_host

[uc_engine:vars]
wazo_distribution = pelican-bookworm
wazo_distribution_upgrade = pelican-bookworm

engine_api_configure_wizard = true
engine_api_root_password = Azertyuiop14!
api_client_name = naradmin
api_client_password = Azerty 

Enfin, on peut installer wazo (oui via ansible !):

naradmin@wazo:~$ ansible-playbook -i inventories/uc-engine uc-engine.yml

Une fois terminée, on peut se connecter sur l'interface web via https://IP_WAZO :

Configuration

Tenant

Par défaut, on arrive dans le tenant master mais aucune configuration doit être faite dedans. Je vais respecter une norme d'un tenant par client. Ainsi, si un client signe pour un centrex, les postes vont être configurés dans le tenant. Et si le client signe pour un trunk SIP ? Le trunk sera crée dans son tenant Wazo quand même !

Dans paramètres globaux -> tenant :

On crée nos tenants pour nos clients :

Context

Il faut comprendre que, comparé à d'autres solutions, Wazo est assez brut de décoffrage. C'est à dire qu'il y a très peu d'abstraction qu'on peut retrouver sur du 3CX par exemple. De ce fait, il faut un peu comprendre comment fonctionne Asterisk pour faire du Wazo.

On commence donc par le principe du diaplan.

Un dialplan est la table de routage de SDA. Par exemple, il permet router l'appel vers le numéro de destination.

Ce qui nous amène au contexte. Chaque diaplan est organisé en section, plus communément appelée contexte.. Ils servent à renforcer la sécurité, à séparer les fonctionnalités ainsi que pour des groupes d'utilisateurs (un context stagiaire par exemple).

Dans mon Wazo, j'ai décidé de faire partir sur 3 :

      • Internal : Appels internes (des extensions par exemple 1000)
      • Incall : Appels entrants (pointer des SDA, numéros de téléphones, vers des utilisateurs)
      • Outcall : Appels sortants (un utilisateur peut contacter une SDA publique).

Pour les créer, il faut aller dans avancé -> contextes :

Ensuite, on peut créer nos contextes. J'ai décidé de partir sur la syntaxe : NOM_CLIENT-TYPE_CONTEXTE. Par exemple, si un client s'appelle Konoha, ca sera Konoha-Internal :

Ensuite, on modifie l'internal pour le lier au contexte outcall. En effet, si on met un utilisateur dans le contexte internal, il ne pourra passer que des appels internes. Or, on a quand même envie que nos utilisateurs puissent passer des appels sortants !

On crée aussi la range des extensions :

Il faut aussi modifier celui qui sert aux appels entrants pour mettre la range des SDA dédiées pour ce client :

Bien évidemment, ces SDA seront changées quand notre transit VOIP nous en donnera (dans quelques épisodes)

Pour l'outcall, pas besoin d'ajouter quoi que soit mis à part le créer !

User

Il existe plusieurs moyens de 'collecter' la téléphonie soit en centrex (téléphone chez le client qui viennent s'enregistrer sur notre PBX) soit via trunk sip (client possède son PBX chez lui et est autonome sur la configuration), soit par d'autres solutions qu'on verra très prochainement ! Pour cet épisode, j'ai décidé de partir sur du centrex car c'est ce que prennent 99% des clients ! On verra les trunk dans le prochain acte.

Donc on crée nos utilisateurs. Dans gestion des utilisateurs -> utilisateurs :

On doit donc mettre les info de bases du clients, le contexte associé (internal), une extension, ici 1002 et choisir SIP en protocole. Pour la template, Wazo en propose plusieurs (Avancée -> Modèles SIP) :

Je ne vais pas faire un focus sur les paramètres qu'apportent la template dans cet épisode. J'en dédierai un sur le protocole SIP et les paramètres qu'on peut apporter dans des trunk etc.

Pour finir, on crée un deuxième utilisateur :

Devices

Il existe deux moyen d'approvisionner des téléphones :

        • Manuellement dans le téléphone
        • Automatiquement via DHCP

Je ne fais pas d'autoprov pour l'instant, on verra ça par la suite ! Donc on a besoin de l'IP et de la MAC de chaque poste pour les créer manuellement dans notre wazo. Mais avant ça, on télécharge les plugins pour nos téléphones. Dans paramètres globaux -> Modules de périphériques :

Il faut prendre ceux compatibles avec vos téléphones. Pour ma part, mon polycom VVX400 c'est wazo-polycom-5.9.2 et pour mon Yealink T46G c'est wazo-yealink-v83.

De plus, je crée une template pour nos téléphones (pour pouvoir régler le fuseau horaire, la langue, etc) :

On peut enfin créer nos devices dans Gestion des utilisateurs -> Périphériques :

Une fois crée, on doit l'edit pour renseigner la template et le module (bon c'est un screen une fois le phone monté, on a pas le modèle fournisseur et la version, on les obtient quand le téléphone est UP sur Wazo) :

Line

Une ligne est un compte SIP pour un utilisateur. On en a crée deux donc on observe bien deux lignes SIP (Gestion des utilisateurs -> lignes) :

Il faut qu'on récupère deux informations très importantes : l'user / password de la ligne SIP !

Par exemple pour la ligne de l'utilisateur Sasuke :

On renseigne ces informations dans la configuration de l'utilisateur dans Authentification :

C'est les logins de la ligne SIP. Le téléphone va donc les avoir dans sa configuration. On peut le comparer au compte PPP qui permet d'authentifier un CPE.

Pour finir, dans l'onglet lignes, on link le device à l'utilisateur :

Plutôt pas mal ! Il nous reste plus qu'à dire aux téléphones que leur serveur SIP c'est le wazo !

Yealink, je suis passé par l'interface web :

Ensuite, on peut appuyer sur le bouton autoprovision now :

Pour mon polycom, l'interface web est buggé sah donc je suis passé par le téléphone en lui même (dans config réseau, autoprov, etc)

Tests d'appels

A partir de mon yealink, j'appelle le numéro 1001 (Naruto vers Sasuke) et un petit check dans les logs (/var/log/asterisk/full) :

Tout d'abord, on observe que l'appel entre bien dans le bon tenant avec le bon contexte et que le numéro destination est bien trouvé :

[2026-02-10 21:48:20.7446] VERBOSE[58292][C-00000009] pbx.c: Executing [1001@ctx-Customer1-internal-4210efd3-1a68-420c-8a9d-bb76612772ee:1] Set("PJSIP/lba52j5v-0000000c", "__WAZO_BASE_CONTEXT=ctx-Customer1-internal-4210efd3-1a68-420c-8a9d-bb76612772ee") in new stack
[2026-02-10 21:48:20.7446] VERBOSE[58292][C-00000009] pbx.c: Executing [1001@ctx-Customer1-internal-4210efd3-1a68-420c-8a9d-bb76612772ee:2] Set("PJSIP/lba52j5v-0000000c", "__XIVO_BASE_EXTEN=1001") in new stack
[2026-02-10 21:48:20.7447] VERBOSE[58292][C-00000009] pbx.c: Executing [1001@ctx-Customer1-internal-4210efd3-1a68-420c-8a9d-bb76612772ee:3] Set("PJSIP/lba52j5v-0000000c", "__WAZO_TENANT_UUID=7455b82a-31fe-48a4-b7d2-2bb01a787ec5") in new stack
[2026-02-10 21:48:20.7447] VERBOSE[58292][C-00000009] pbx.c: Executing [1001@ctx-Customer1-internal-4210efd3-1a68-420c-8a9d-bb76612772ee:4] Gosub("PJSIP/lba52j5v-0000000c", "contextlib,entry-exten-context,1") in new stack

Ensuite, on peut voir qui appelle qui et que l'origine est interne :

[2026-02-10 21:48:20.7451] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:1] Set("PJSIP/lba52j5v-0000000c", "WAZO_DSTID=2") in new stack
[2026-02-10 21:48:20.7451] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:2] Set("PJSIP/lba52j5v-0000000c", "WAZO_RING_TIME=") in new stack
[2026-02-10 21:48:20.7452] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:3] Set("PJSIP/lba52j5v-0000000c", "WAZO_USER_MOH_UUID=") in new stack
[2026-02-10 21:48:20.7452] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:4] Set("PJSIP/lba52j5v-0000000c", "WAZO_DST_EXTEN_ID=3") in new stack
[2026-02-10 21:48:20.7453] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:5] Set("PJSIP/lba52j5v-0000000c", "WAZO_PRESUBR_GLOBAL_NAME=USER") in new stack
[2026-02-10 21:48:20.7453] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:6] Set("PJSIP/lba52j5v-0000000c", "WAZO_SRCNUM=1002") in new stack
[2026-02-10 21:48:20.7454] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:7] Set("PJSIP/lba52j5v-0000000c", "WAZO_DSTNUM=1001") in new stack
[2026-02-10 21:48:20.7454] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:8] Set("PJSIP/lba52j5v-0000000c", "WAZO_CONTEXT=ctx-Customer1-internal-4210efd3-1a68-420c-8a9d-bb76612772ee") in new stack
[2026-02-10 21:48:20.7454] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:9] Set("PJSIP/lba52j5v-0000000c", "WAZO_CHANNEL_DIRECTION=to-wazo") in new stack
[2026-02-10 21:48:20.7455] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:10] Set("PJSIP/lba52j5v-0000000c", "__WAZO_CALLORIGIN=intern") in new stack
[2026-02-10 21:48:20.7455] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:11] Set("PJSIP/lba52j5v-0000000c", "__WAZO_FWD_REFERER=user:2") in new stack
[2026-02-10 21:48:20.7456] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:12] UserEvent("PJSIP/lba52j5v-0000000c", "User,CHANNEL: PJSIP/lba52j5v-0000000c, XIVO_USERID: 3,WAZO_SRCNUM: 1002,WAZO_CALLORIGIN: intern,XIVO_DSTID: 2,WAZO_USERID: 3,WAZO_DSTID: 2") in new stack
[2026-02-10 21:48:20.7456] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:13] GotoIf("PJSIP/lba52j5v-0000000c", "?:noblindxfer") in new stack
[2026-02-10 21:48:20.7457] VERBOSE[58292][C-00000009] pbx_builtins.c: Goto (user,s,15)
[2026-02-10 21:48:20.7457] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:15] Set("PJSIP/lba52j5v-0000000c", "WAZO_FWD_REFERER_TYPE=user") in new stack
[2026-02-10 21:48:20.7457] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:16] Set("PJSIP/lba52j5v-0000000c", "WAZO_REAL_FROMGROUP=0") in new stack
[2026-02-10 21:48:20.7458] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:17] Set("PJSIP/lba52j5v-0000000c", "WAZO_REAL_FROMQUEUE=0") in new stack
[2026-02-10 21:48:20.7458] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:18] AGI("PJSIP/lba52j5v-0000000c", "agi://127.0.0.1/incoming_user_set_features")  in new stack

Notre utilisateur est autorisé à passer des appels :

[2026-02-10 21:48:20.8136] VERBOSE[58292][C-00000009] pbx.c: Executing [allow@wazo-user_permission_check:1] NoOp("PJSIP/lba52j5v-0000000c", "User allowed to make call") in new stack

Wazo comprend qu'il doit router l'appel vers l'endpoint SIP de Sasuke :

[2026-02-10 21:48:20.8408] VERBOSE[58292][C-00000009] pbx.c: Executing [s@user:50] Dial("PJSIP/lba52j5v-0000000c", "PJSIP/9mveh5ll/sip:9mveh5ll@192.168.1.22,30,b(wazo-pre-dial-hooks^s^1)") in new stack

Le poste de sasuke sonne :

[2026-02-10 21:48:20.9485] VERBOSE[58292][C-00000009] app_dial.c: PJSIP/9mveh5ll-0000000d is ringing

L'utilisateur sasuke répond à l'appel :

[2026-02-10 21:48:21.8091] VERBOSE[58292][C-00000009] app_dial.c: PJSIP/9mveh5ll-0000000d answered PJSIP/lba52j5v-0000000c

Les deux utilisateurs rejoignent le même bridge pour qu'ils puissent communiquer ensemble :

[2026-02-10 21:48:21.8096] VERBOSE[58311][C-00000009] bridge_channel.c: Channel PJSIP/9mveh5ll-0000000d joined 'simple_bridge' basic-bridge 
[2026-02-10 21:48:21.8097] VERBOSE[58292][C-00000009] bridge_channel.c: Channel PJSIP/lba52j5v-0000000c joined 'simple_bridge' basic-bridge 

Et lors du raccrochage :

[2026-02-10 21:48:36.0778] VERBOSE[58311][C-00000009] bridge_channel.c: Channel PJSIP/9mveh5ll-0000000d left 'simple_bridge' basic-bridge 
[2026-02-10 21:48:36.0781] VERBOSE[58292][C-00000009] bridge_channel.c: Channel PJSIP/lba52j5v-0000000c left 'simple_bridge' basic-bridge 

On peut aussi voir dans les CDR que l'appel a bien eu lieu :

Et une capture de flux :

Et en graphique de flux (où 192.168.1.210 est l'IPBX et la .199 et .22 sont des téléphones) :

Conclusion

Pour un premier épisode sur la téléphonie et plus globalement dans ma carrière pro, je suis assez content du résultat !

Au final, on a réussi à monter notre IPBX en mode centrex et faire notre premier test d'appel entre deux téléphones dans le contexte internal.

Bon c'est clairement pas suffisant pour pouvoir vendre ça aux clients ... Au final, notre solution ne fonctionne pas pour des clients possédant déjà leur PBX donc prochain épisode, on regarde comment fonctionnent les trunk SIP sur Wazo ! Ensuite, on pourra s'attaquer au SBC et à notre trunk opérateur transit.

De plus, utiliser Wazo sans les API ne sert à pas grand chose ... Mais on va attendre de monter toute l'infra avant de commencer à parler automatisation mais imaginez qu'on puisse administrer un tenant d'un client à partir de notre ERP odoo ... Le game changer nan ? 😄

Je termine sur un mot de fin : cherche travail service infrastructure opérateur pitié, j'ai envie de m'amuser