Microsoft Teams for Incredible PBX®     

Licensed for use pursuant to GPLv2+

Portions Copyright © 2021-2023, Ward Mundy & Associates LLC

Support: https://wiki.incrediblepbx.com

Overview

Microsoft Setup

Getting Started

Add the Domain

Add the "Activation User"

Add the SBC, Voice Route, Dial Plan, and Voice Routing Policy

PBX Setup

Patch Asterisk

Commands

Setup domain (FQDN) and certificate

DNS

Certificate

Setup PJSIP

Transport Settings

Trunk Settings

Allow the Microsoft Teams SIP proxies through the Incredible PBX firewall

Trunk/SBC Verification

Add the [from-teams] context

Setting up Teams PBX extensions

PBX

Microsoft configuration

Create and license a user

Assign the phone number

Test Calling

Appendices

Appendix 1: VitalPBX patch to PJSIP to support Microsoft Teams signaling

Appendix 2: Alternate configuration

Voicemail

Overview

This guide will walk through the steps for setting up Microsoft Teams users as extensions to Incredible PBX. The guide consists of three major sections:

·       Setup of the Microsoft 365/Teams environment, including licensing, SBC configuration, and setup of the Direct Routing environment

·       Setup of Incredible PBX trunking and dial plan

·       Adding Teams extensions to Incredible PBX

At the end of the tutorial, you will be able to use a Teams client (desktop, mobile, or both at once) as an extension to Incredible PBX that functions almost identically to a SIP phone extension.

Microsoft Setup

Getting Started

UPDATE: Minimum Microsoft license requirements as of May, 2023 are as follows: Microsoft 365 Business Basic ($6/mo./user) or Microsoft Teams Essentials ($4/mo./user) PLUS Microsoft Teams Phone Standard ($8/mo./user). In the Admin Center…

Add the Domain

Expand the menu on the left, open Settings and go to Domains. Here we add the domain (FQDN) that will be assigned to the PBX or is already assigned.

Click Add Domain and type the domain (FQDN) of the PBX. We are using "mypbx.team".

Note that you must use a name for which you have DNS control. Dynamic DNS is OK as long as you can also manually add DNS records (see next step).

Use the TXT record method to verify.

Add the DNS record before clicking Verify.

After verification, click Continue.

Deselect the Exchange box. We do not need to add any more records at this time. Click Continue.

Add the "Activation User"

Switch to the Users - Active Users screen. Before adding the SBC, we have to create an "Activation User" which is a Teams-licensed user that has the same domain as the SBC (PBX).

Be sure to pick the PBX's domain name for this user.

This user will never log in, so do not worry about password details.

The user must be assigned a license with Teams (MS 365 / Office 365). TIP: if you are doing this in an established Microsoft tenant, you can temporarily unassign a license from another user and assign it to the Activation User. The license can be unassigned from this user once the SBC setup is done.

Do not assign a Phone System license.

Click Next until finished.

Add the SBC, Voice Route, Dial Plan, and Voice Routing Policy

The next steps are performed in Microsoft Teams administration. Go to https://admin.teams.microsoft.com and log in with your Global Admin user (the one you created at initial sign-up).

Expand the menu on the left, go to Voice, and Direct Routing.

Click + Add under SBCs.

Specify the details for the SBC. Enter the FQDN (the domain name you just set up), toggle Enabled to On, set the signaling port to 5061, enable SIP Options, enable PAI, and set a reasonable concurrent call capacity. Click Save.

After saving your new SBC, you will be back at the Direct Routing screen and see your SBC listed. After setting up the PBX, we will check back here to make sure the SBC is in a healthy state.

Now go to the Voice Routes tab on this screen and click LocalRoute to edit it.

Change the Dialed number pattern to .*

This pattern accepts any digits, *, or # that are dialed; any dial string will be routed to our PBX.

Click Add SBCs and pick your SBC.

Click Add PSTN Usage, create a new one and name it whatever you like, select it, and save & apply.

Lastly, save the voice route and you are returned to the Direct Routing screen.

Navigate to Dial Plans and edit Global.

Click Add to add a Normalization Rule. Call it "No Modification", use the Advanced options, and enter the regex ^(.*)$. Set the translation to $1 .

The reason for this rule is that by default, Teams will apply transformations related to your E.164 region. We do not want any modifications done on the dialed numbers; they should be passed through, exactly as dialed, to our PBX.

After saving the Normalization Rule, save the Dial Plan.

Navigate to Voice Routing Policies; add a new one. Name it MyVoiceRoutingPolicy and click the button to add your PSTN Usage Record, created earlier. Click Save.


PBX Setup

Patch Asterisk

We will use the Asterisk PJSIP code modifications from VitalPBX, released to the public per GPL licensing requirements, to enable Asterisk to communicate with Microsoft's SIP proxies. The patch is included in appendix 1 of this document, originally from this Github commit: https://github.com/asterisk/asterisk/commit/203b6ebb9b976a496e524fc41b3b4f2b7480fff1

This patch has been tested against Asterisk 20.2.1 and 20.3.0 using the latest release of Incredible PBX 2027.

Commands

To include this patch with Incredible PBX 2027 with Asterisk 20, issue the following commands after logging into your server as root:

 

cd /root

wget http://incrediblepbx.com/upgrade-asterisk20T

chmod +x upgrade-asterisk20T

./upgrade-asterisk20T

Setup domain (FQDN) and certificate

DNS

Configure the DNS A record for your PBX if you have not done so already. We are using mypbx.team as the domain name configured earlier for Teams and which will point to our PBX.

mypbx.team → A → x.x.x.x

Certificate

In Incredible PBX, go to Admin → Certificate Management:

·       New Certificate → Generate Let's Encrypt

·       Certificate Host Name: mypbx.team (our example)

·       Briefly open the firewall during certificate generation:

·       iptables -I INPUT -m tcp -p tcp --dport 80 -j ACCEPT , click Generate Certificate, when complete, iptables -D INPUT 1

·       Because of the need for manual firewall manipulation, automatic renewal will fail. Return to this screen within 90 days to manually renew the certificate by clicking the edit button next to the Let's Encrypt certificate and clicking Update (with the firewall temporarily opened).

·       Once generated, click the Default checkbox in the Certificate Management screen to make this certificate the default

Setup PJSIP

Transport Settings

In Asterisk SIP Settings → General SIP Settings, click Detect Network Address to make sure FreePBX knows about its external IP.

In the SIP Settings [chan_pjsip] tab, pick the certificate you just generated, set SSL Method to TLSv1.2, and set Verify Client and Verify Server to Yes. Set the TLS transport to Yes and click Submit.

Go back into the SIP Settings [chan_pjsip] tab and scroll to the bottom; confirm that the TLS Port to Listen On is set as 5061.

Click the Apply Config button at the top. Don't restart Asterisk yet.

Edit /etc/asterisk/pjsip.transports_custom_post.conf to add the MS Teams domain parameter, substituting in your FQDN:

[0.0.0.0-tls](+)

ms_signaling_address=mypbx.team

Now restart Asterisk with fwconsole restart to bring up the new PJSIP TLS transport with Microsoft signaling capability.

Verification: check the PJSIP TLS transport on the asterisk console

# asterisk -r

mypbx-team*CLI> pjsip show transport 0.0.0.0-tls

Transport:  <TransportId........>  <Type>  <cos>  <tos>  <BindAddress....................>

==========================================================================================

Transport:  0.0.0.0-tls               tls      3     96  0.0.0.0:5061

 ParameterName              : ParameterValue

 ===============================================================

 allow_reload               : true

 async_operations           : 1

 bind                       : 0.0.0.0:5061

 ca_list_file               : /etc/ssl/certs/ca-certificates.crt

 ca_list_path               :

 cert_file                  : /etc/asterisk/keys/mypbx.team.pem

 cipher                     :

 cos                        : 3

 domain                     :

 external_media_address     : 1.2.3.4

 external_signaling_address : 1.2.3.4

 external_signaling_port    : 0

 local_net                  : 10.0.0.0/255.255.255.0

 method                     : tlsv1_2

 ms_signaling_address       : mypbx.team

 password                   :

 priv_key_file              : /etc/asterisk/keys/mypbx.team.key

 protocol                   : tls

 require_client_cert        : No

 symmetric_transport        : false

 tos                        : 96

 verify_client              : Yes

 verify_server              : Yes

 websocket_write_timeout    : 100

Trunk Settings

Go to Connectivity → Trunks. Add a PJSIP Trunk.

In the General tab, name the trunk "msteams" and then move to the PJSIP Settings tab.

Apply the following settings:

·       Authentication: None

·       Registration: None

·       SIP Server: sip.pstnhub.microsoft.com

·       SIP Server Port: 5061

·       Context: from-teams

·       Transport: 0.0.0.0-tls

Move to the Advanced tab and adjust the following (the rest can remain default):

·       User = Phone =         Yes

·       From Domain =         your domain

·       Server URI =                 sip:sip.pstnhub.microsoft.com:5061\;transport=tls

·       Match (Permit) =         sip-all.pstnhub.microsoft.com

·       Rewrite Contact =         Yes

·       Media Encryption =         SRTP via in-SDP

Move to the Codecs tab and make sure at least ulaw is selected.

Save the trunk and Apply Config.

Allow Microsoft Teams SIP proxies through Incredible PBX firewall

Use add-fqdn to allow the Microsoft SIP proxies to send us traffic:

./add-fqdn teamsproxies sip-all.pstnhub.microsoft.com

Select "2" for SIP (TCP). This selection covers TLS (a TCP protocol) as well.

Trunk/SBC Verification

Go to Reports → Asterisk Info and scroll down to PJSIP to verify that endpoint "msteams" shows status "Avail":

Go to the Teams administration site (https://admin.teams.microsoft.com), Voice → Direct Routing and you should now see your SBC showing "Active" in the TLS connectivity status:

Note that it may take a few minutes for this status to change. Also, it may take several hours until you see the SIP Options status change from Warning to Active.

Add the [from-teams] context

We are using a custom context for our Teams trunk that removes the + that comes in on our Teams extensions' caller IDs.

Edit /etc/asterisk/extensions_custom.conf.

At the top or bottom, add the following short dialplan:

[from-teams]

; strip + from caller ID coming from Teams

exten => _.,1,Set(CALLERID(num)=${CALLERID(num):1})

same => n,Goto(from-internal,${EXTEN},1)

Save the file and inform Asterisk of the change with a dialplan reload command:

asterisk -rx "dialplan reload"

Setting up Teams PBX extensions

PBX

Go to Applications → Extensions to add a new extension.

Add a new Custom Extension:

Assign an extension number and name as usual:

Configure the Voicemail and FM/FM tabs as you like. In the Advanced tab, set the Dial field to PJSIP/extensionnumber@msteams. For extension 710, it would look like this:

Save the extension and Apply Config.

Microsoft configuration

Create and license a user

Create the user in your Microsoft tenant, or license an existing user with a Teams license (any MS 365 / Office 365) and a Phone System license. Your users do not have to have the same domain name as the SBC/PBX domain; they only have to be within the same tenant/organization. The new user is added from the admin console at https://admin.microsoft.com.

Click Next until finished.

Assign the phone number

After the user is assigned a Phone System license, he needs to have "Enterprise Voice" enabled and the extension number assigned. This is done in Power Shell.

You can use Power Shell from a Windows computer if you download the Microsoft Teams Power Shell connector. (See https://docs.microsoft.com/en-us/microsoftteams/teams-powershell-install) A more convenient way to access Power Shell is through the Azure portal on the web.

Go to https://portal.azure.com and log in with your Global Administrator account.

Click the >_ symbol to the right of the search bar to bring up Cloud Shell, and then choose Power Shell.

If you don't have an Azure subscription, you will be directed to the following page where you can click Start Free. Follow the prompts to get through verification and free account signup.

If prompted to create a storage location, accept this (free during first month with credit; a penny or two per month afterward).

Expand the Power Shell window.

Import the Microsoft Teams module and log in: (the Power Shell prompt will be represented by PS> ; issue the commands as follows)

PS> import-module MicrosoftTeams

PS> Connect-MicrosoftTeams -usedeviceauthentication

 

Follow the one-line instruction that follows in order to log in using a separate browser tab. Proceed through the prompts until it says "You have signed in…" We use the "device authentication" method because it works with 2FA logins, should you decide to enable them.

Back in Power Shell, you will see your account ID listed and be returned to the prompt.

Issue the following command to enable your user, replacing the identity field with your user's login ID and the onpremlineuri field with your user's PBX extension (set up in the previous section), in the format of tel:extensionnumber:

PS> Set-CsPhoneNunberAssignment -identity "captain@mypbxteam.onmicrosoft.com" -PhoneNumber “xxx” -PhoneNumberType DirectRouting

 

You may receive an error like: Set-CsPhoneNumberAssignment: Management object not found for identity "captain@mypbxteam.onmicrosoft.com". If so, wait a little while and try again. There can be propagation delays with the licensing.

 

Grant the user the Voice Routing Policy you set up previously by issuing the Grant-CsOnlineVoiceRoutingPolicy command:

PS> Grant-CsOnlineVoiceRoutingPolicy -identity "captain@mypbxteam.onmicrosoft.com" -policyname "MyVoiceRoutingPolicy"

You are ready to test.

Test Calling

Install Microsoft Teams on desktop or mobile if you don't already have it and then log in. You can also use Teams on the web by navigating to https://teams.microsoft.com. Use a private/incognito window so that you can log in with a different account than your tenant's Global Administrator.

Navigate to the Calls tab. You should see a dial pad and your extension number. If you see a screen without the dial pad, the Microsoft provisioning is still in progress.

The dial pad is not yet shown

Wait 30 minutes or so and restart Teams. Microsoft notes that it may take up to 4 hours after Enterprise Voice has been enabled and the Voice Routing Policy has been applied for the dial pad to appear.

Teams with dial pad on desktop

Teams mobile with dial pad

   

Use the dial pad to call other PBX extensions, PSTN numbers, or internal feature codes such as *97. Call to your Teams extension from a SIP phone. If you have both a desktop and a mobile client logged in to Teams, both will ring on an incoming call.

Your Teams extension can be added to ring groups, targeted directly by an Inbound Route, and generally participate in Incredible PBX functions.

Note: To add a Teams extension to a ring group, treat it as if you were adding an external number; e.g. add it to the ring group as 710#.


Appendices

Appendix 1: VitalPBX patch to PJSIP to support Microsoft Teams signaling

From 203b6ebb9b976a496e524fc41b3b4f2b7480fff1 Mon Sep 17 00:00:00 2001

From: Jose Rivera <ing.joserivera26@gmail.com>

Date: Mon, 17 May 2021 14:23:37 -0600

Subject: [PATCH] Add support for MS Team implementation

The variable ms_signaling address is now added. This variable is not stringify nor change from domain to IP.

---

 include/asterisk/res_pjsip.h     |  2 ++

 res/res_pjsip.c                  |  6 ++++++

 res/res_pjsip/config_transport.c | 32 ++++++++++++++++++++++++++++++++

 res/res_pjsip_nat.c              | 19 +++++++++++++++++--

 4 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h

index 2020ca8782c..6f155f893af 100644

--- a/include/asterisk/res_pjsip.h

+++ b/include/asterisk/res_pjsip.h

@@ -188,6 +188,8 @@ struct ast_sip_transport {

                 AST_STRING_FIELD(external_media_address);

                 /*! Optional domain to use for messages if provided could not be found */

                 AST_STRING_FIELD(domain);

+        /*! MS Team Variable */

+        AST_STRING_FIELD(ms_signaling_address);

                 );

         /*! Type of transport */

         enum ast_transport type;

diff --git a/res/res_pjsip.c b/res/res_pjsip.c

index 775b63f8d1a..b0f2f2268fa 100644

--- a/res/res_pjsip.c

+++ b/res/res_pjsip.c

@@ -1688,6 +1688,9 @@

                                 <configOption name="external_signaling_address">

                                         <synopsis>External address for SIP signalling</synopsis>

                                 </configOption>

+                                <configOption name="ms_signaling_address">

+                    <synopsis>MS Team address for SIP signalling</synopsis>

+                </configOption>

                                 <configOption name="external_signaling_port" default="0">

                                         <synopsis>External port for SIP signalling</synopsis>

                                 </configOption>

@@ -2512,6 +2515,9 @@

                                 <parameter name="ExternalSignalingAddress">

                                         <para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='external_signaling_address']/synopsis/node())"/></para>

                                 </parameter>

+                                <parameter name="MSTeamSignalingAddress">

+                    <para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='ms_signaling_address']/synopsis/node())"/></para>

+                </parameter>

                                 <parameter name="ExternalSignalingPort">

                                         <para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='external_signaling_port']/synopsis/node())"/></para>

                                 </parameter>

diff --git a/res/res_pjsip/config_transport.c b/res/res_pjsip/config_transport.c

index 830b03832d9..b6a66a1fdaf 100644

--- a/res/res_pjsip/config_transport.c

+++ b/res/res_pjsip/config_transport.c

@@ -953,6 +953,35 @@ static int privkey_file_to_str(const void *obj, const intptr_t *args, char **buf

         return 0;

 }

 

+/*! \brief Custom handler for ms team parameter */

+static int transport_ms_signaling_address_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)

+{

+        struct ast_sip_transport *transport = obj;

+    RAII_VAR(struct ast_sip_transport_state *, state, find_or_create_temporary_state(transport), ao2_cleanup);

+

+    if (!state) {

+                    return -1;

+    }

+

+    if (ast_strlen_zero(var->value)) {

+        /* Ignore empty options */

+        return 0;

+    }

+

+        ast_string_field_set(transport, ms_signaling_address, var->value);

+

+    return 0;

+}

+

+static int transport_ms_signaling_address_to_str(const void *obj, const intptr_t *args, char **buf)

+{

+        const struct ast_sip_transport *transport = obj;

+

+        *buf = ast_strdup(transport->ms_signaling_address);

+

+        return 0;

+}

+

 /*! \brief Custom handler for turning a string protocol into an enum */

 static int transport_protocol_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)

 {

@@ -1650,6 +1679,9 @@ int ast_sip_initialize_sorcery_transport(void)

         ast_sorcery_object_field_register(sorcery, "transport", "domain", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, domain));

         ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_server", "", transport_tls_bool_handler, verify_server_to_str, NULL, 0, 0);

         ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_client", "", transport_tls_bool_handler, verify_client_to_str, NULL, 0, 0);

+

+        ast_sorcery_object_field_register_custom(sorcery, "transport", "ms_signaling_address", "", transport_ms_signaling_address_handler, transport_ms_signaling_address_to_str, NULL, 0, 0);

+

         ast_sorcery_object_field_register_custom(sorcery, "transport", "require_client_cert", "", transport_tls_bool_handler, require_client_cert_to_str, NULL, 0, 0);

         ast_sorcery_object_field_register_custom(sorcery, "transport", "method", "", transport_tls_method_handler, tls_method_to_str, NULL, 0, 0);

 #if defined(PJ_HAS_SSL_SOCK) && PJ_HAS_SSL_SOCK != 0

diff --git a/res/res_pjsip_nat.c b/res/res_pjsip_nat.c

index 1b5fdd12603..bbca375aaa8 100644

--- a/res/res_pjsip_nat.c

+++ b/res/res_pjsip_nat.c

@@ -32,6 +32,7 @@

 #include "asterisk/res_pjsip_session.h"

 #include "asterisk/module.h"

 #include "asterisk/acl.h"

+#include "asterisk/strings.h"

 

 /*! URI parameter for original host/port */

 #define AST_SIP_X_AST_ORIG_HOST "x-ast-orig-host"

@@ -445,7 +446,14 @@ static pj_status_t process_nat(pjsip_tx_data *tdata)

                         pjsip_method_cmp(&cseq->method, &pjsip_register_method)) {

                         /* We can only rewrite the URI when one is present */

                         if (uri || (uri = nat_get_contact_sip_uri(tdata))) {

-                                pj_strdup2(tdata->pool, &uri->host, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));

+

+                                /* Do not stringify the signalling address when using MS Teams */

+                                if (!ast_strlen_zero(transport->ms_signaling_address)) {

+                                        pj_strdup2(tdata->pool, &uri->host, transport->ms_signaling_address);

+                                }else{

+                                        pj_strdup2(tdata->pool, &uri->host, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));

+                                }

+

                                 if (transport->external_signaling_port) {

                                         uri->port = transport->external_signaling_port;

                                         ast_debug(4, "Re-wrote Contact URI port to %d\n", uri->port);

@@ -455,7 +463,14 @@ static pj_status_t process_nat(pjsip_tx_data *tdata)

 

                 /* Update the via header if relevant */

                 if ((tdata->msg->type == PJSIP_REQUEST_MSG) && (via || (via = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL)))) {

-                        pj_strdup2(tdata->pool, &via->sent_by.host, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));

+

+                        /* Do not stringify the signalling address when using MS Teams */

+                        if (!ast_strlen_zero(transport->ms_signaling_address)) {

+                pj_strdup2(tdata->pool, &via->sent_by.host, transport->ms_signaling_address);

+            }else{

+                pj_strdup2(tdata->pool, &via->sent_by.host, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));

+            }

+

                         if (transport->external_signaling_port) {

                                 via->sent_by.port = transport->external_signaling_port;

                         }


Appendix 2: Alternate configuration

Voicemail

Teams users can use Microsoft's integrated voicemail service instead of Incredible PBX voicemail. Be sure to enable only one or the other--not both.

In the set-csuser command, change -hostedvoicemail $false to -hostedvoicemail $true. The user then sets up voicemail in the Teams client:

Microsoft voicemail includes speech-to-text transcription and direct integration with Outlook.