Introduction
Alexey Vasilyev posted an article to the OpenSIPS blog site in September 2019 which explained how to use OpenSIPS as an SBC for Microsoft Teams Direct Routing:
OpenSIPS as MS Teams SBC – Drops of wisdom, knowledge and news from OpenSIPS
I am sure the OpenSIPS community is grateful to Alexey for sharing his experience, thereby opening the way for some of us to build and deploy OpenSIPS as an interface to Teams. I have no wish to steal his thunder and will do my best to avoid duplicating anything he said. This article is designed to be read alongside that original blog post, simply highlighting a number of issues that can easily catch out anyone attempting to build a Teams SBC with OpenSIPS. I will also try to clarify a couple of points that may not immediately be clear from the original post.
What you will read here is based on my experience building a Teams SBC for a client and also helping one or two fellow travellers to resolve issues they encountered.
About using the Fully Qualified Domain Name (FQDN)
Match the name throughout
Unless you are using the multi-tenant solution, the same FQDN needs to be used everywhere:
- As a domain configured within MS Teams admin (or Office365)
- In the host ID (subject CN) field of the TLS certificates used by OpenSIPS
- As a local or home domain in OpenSIPS (e.g. using the “alias” directive)
- In the Record-Route headers of proxied SIP requests
- In the Contact header of OPTIONS requests that OpenSIPS sends to Teams
- In DNS – it needs to resolve to the address of your SBC
Should we use it in Contact or Record-Route headers?
Teams expects to see the FQDN in the Contact header of the initial request, unless there is a Record-Route header present. If there is a Record-Route header, then it only expects to see the FQDN in that. I do not believe it will check for the FQDN in sequential (in-dialogue) requests, unless it possibly could check the topmost Via header.
This means that, if OpenSIPS is configured to behave as a Proxy server, you must insert a Record-Route header in the initial request using the FQDN. Just supposing you re-configured OpenSIPS to act more like an endpoint instead of a proxy (e.g. by using the B2BUA or Topology Hiding module), then it would be necessary to make sure the Contact header contained the FQDN.
In the case of OPTIONS pings sent by OpenSIPS to Teams, these behave much more like they come from an endpoint than a proxy. That is why the OPTIONS pings need to have the FQDN in the Contact header.
FQDN’s in Record-Route headers – The July 2020 update
This is simply a clarification of the update that Alexey posted following some changes in the behaviour of the Teams interconnection. Before July 2020, it was only necessary to use the FQDN in the Record-Route headers of INVITE requests being sent to Teams. After that date, it became necessary to use FQDN in INVITE’s received from Teams too. Here is some pseudo-code to illustrate how you would handle it:
Pseudo-code explaining how to insert RR headers:
if (INVITE-from-Teams-Proxy-to-us) {
record_route_preset(“IP:port”, “SBC_FQDN:5061;transport=tls”);
add_rr_param(“;r2=on”);
} else if (INVITE-from-us-to-Teams-Proxy) {
record_route_preset( “SBC_FQDN:5061;transport=tls”, “IP:port”);
add_rr_param(“;r2=on”);
} else
record_route();
Where “IP:port” is the address you want your servers or gateways to use when they send packets to the SBC. [In case it is not clear what I mean, “your servers and gateways” refers to the downstream destinations for calls coming from Teams or the upstream source of calls being sent to Teams.] If the SBC is acting as a border controller at the network edge using two network interfaces (WAN and LAN), then “IP:port” would probably be the LAN address of your SBC host server.
In some situations, the FQDN could be used instead of “IP:port” but only if all your other servers and gateways are able to reach the SBC using the FQDN. If, by chance, you find that the first and second parameter strings being passed to record_route_preset() are identical, then you do not need to use double RR headers – instead, you could just pass it one parameter and remove the line that adds “;r2=on”.
Terminology and multi-tenant solutions
A fairly relaxed approach is adopted for terminology like “domain” and “FQDN” in this article and in Alexey’s too. In MS Teams the “domain” you define and verify takes a value that typically resolves to one specific host – your SBC. That may not quite seem right to a DNS terminology pedant who could argue that the domain is the bit after the first dot: <hostname>.<domain>. Blame Microsoft if you don’t like it.
The multi-tenant solution available in Teams introduces some new terminology:
- Carrier
- Tenant
- Subdomain
The carrier is the service provider who owns and operates the Teams SBC. One carrier can use their SBC to offer a service to many tenants, for use with Teams Direct Routing. The base domain name is the one used by the carrier and it resolves to the address of the SBC. Set up the carrier account first. Each tenant must be assigned a subdomain – i.e. a FQDN with one extra level added in front of the base domain. The subdomain names must also resolve in DNS to the address of the carrier SBC. The security certificate used on the SBC for TLS needs to be a wildcard certificate so it will work with any tenant subdomain. In the script for your OpenSIPS SBC, use the base domain name in the OPTIONS pings you generate, but use the appropriate tenant-specific subdomain in the Record-Route headers you add to INVITE requests. Your SBC will be able to detect which tenant account to use when it receives a new SIP request from Teams because the subdomain will appear in the R-URI and the To header. There is no magic setting in the MS Teams configuration to say “I am a carrier” or “I am a tenant”, so don’t waste time looking for one. It is all explained reasonably well in Microsoft’s wiki article here:
https://docs.microsoft.com/en-us/microsoftteams/direct-routing-sbc-multiple-tenants
Don’t mess with the received Contact header
It is really important that you do not call fix_nated_contact() at any time when handling a SIP request received from Teams. Equally, you must not call it in onreply_route for a SIP response received from Teams. This is because the Contact header sent by Teams contains important information about the call. If you rewrite the header then you will destroy that information and break the call setup.
Beware of very large packets
If your SBC is performing transport protocol conversion from TLS to UDP, be aware of the risk of packet fragmentation. The packets from Teams can be quite large, but that doesn’t matter when they are delivered over TLS. If you convert them to UDP, it is possible that your network may struggle with fragmentation, especially if the packets are being sent out beyond your internal network.
If you think this is causing problems, consider changing to TCP instead of UDP.
Make sure you support encrypted media
Teams requires the media to be RTP/SAVP. You can use rtpengine for transcoding if your core servers are unable to support it. Rtpengine can convert between unencrypted and encrypted RTP. However, it is not essential to use rtpengine provided your SBC is passing the SIP traffic to a server that does support it and the media ports are accessible through your firewall.
Teams is very fussy
I have now seen several examples of cases where call setup starts out looking okay, but the call fails to be established correctly because there is a very minor infringement of the published SIP protocol standards. This typically manifests itself as the ACK being ignored which usually means there is no audio and then the call is dropped after about 20 seconds. The SIP handling within Microsoft’s core servers is sometimes slightly quirky, but always conforms to the standards. It is also extremely unforgiving of anything it receives that is even slightly outside those standards. Here are a couple of examples:
Kamailio and the lr parameter
When using loose routing, a proxy adding a Record-Route header must include the “;lr” parameter to indicate that loose routing is being used. OpenSIPS and Kamailio both do this automatically when you call the relevant RR module function – either record_route() or record_route_preset().
The RR module in Kamilio has an option that no longer exists in OpenSIPS (it did exist in OpenSIPS until about v1.6). That option is called “enable_full_lr”. When set, it causes Kamailio to add the parameter in a slightly different format – “;lr=on” instead of “;lr”. It is a really subtle difference, but it is enough to break your call when interacting with MS Teams. So if there is a Kamailio in the path – and there could be if you are using 2600Hz Kazoo – then it is essential that “enable_full_lr” is explicitly disabled.
The value of the To-tag
When you send an INVITE request to Teams, you would hope to see some responses like this coming back:
- 100 Trying
- 180 Ringing
- 183 Session Progress
- 200 OK
If you look at these in detail, you will see that, other than the “100 Trying”, they all have a To-tag. However, the value of the To-tag is not consistent even though they all belong to the same dialogue. This behaviour is a little unusual, but does not break any standards and may actually reflect how Microsoft handles the call.
The SBC will pass all the responses back to the UAC that first generated the initial INVITE. When the UAC receives the 200 OK, it must send an ACK message to Teams via the SBC. This is where the behaviour of the UAC device can make a difference because it must put the correct To-tag in the ACK. The correct one is the one that was in the 200 OK. I have seen a case where voipswitch was sending the wrong To-tag in its ACK. It was actually using the tag from the first “180 Ringing”.
REFER handling and putting calls on hold
It is very gratifying to find that your SBC is able to successfully set up inbound and outbound calls to Teams. Unfortunately, that is not the end of the story. You now need to consider what happens when the users click the Hold button or attempt to transfer a call. Bear in mind that they may attempt to transfer a call to another Teams user (I shall refer to these as internal transfers) or to a PSTN number (I refer to these as trunk-to-trunk transfers).
The simplest and easiest way to deal with all transfers is to remove REFER from the Allow header during call setup. OpenSIPS very conveniently provides some functions for doing just that. Here is a sample snippet of code that could be used to strip out REFER from the Allow header of the INVITE:
if (list_hdr_has_option("Allow", "REFER")) {
# Remove Refer as a supported option
list_hdr_remove_option("Allow", "REFER");
}
What is the effect of this? It means that Teams will never send us a REFER request. This forces Teams to handle all transfers with the minimum reference to our SBC. At the start of any transfer, Teams will send us a re-INVITE to put the call on hold. Internal transfers are then handled entirely within Teams and our SBC will not be troubled again until the transfer is complete and the call needs to be taken off hold. Trunk-to-trunk transfers are likely to result in our SBC also seeing a new outbound call from Teams, although this would depend on the Teams dial plan. Our SBC simply needs to handle this new outbound call exactly as it would any other outbound call – it does not need to match the outbound call leg with an existing call even though there may actually be a relationship between them. One disadvantage here is that trunk-to-trunk transfers result in tromboning. If REFER had been used, then it would have been possible to reduce or eliminate the tromboning.
So why don’t we want Teams to send us a REFER? There’s a simple answer – when it sends the REFER for an internal transfer, we will not be able to process it because the username in the URI is blank. In this situation, it is not completely impossible to handle the REFER, but it is quite complicated and requires extra levels of integration outside the scope of the simple SIP dialogue and certainly outside the scope of this article.
Disabling REFER has one more disadvantage in addition to tromboning. It also means that your SIP dialogues alone will not be sufficient to accurately track the status – or presence – of individual Teams users. Your SBC might regard a Teams user status as “talking on an inbound call” when in fact the user is free and that call was transferred to someone else. It also will not know that the “someone else” is now talking on a call. You might want to consider using the Graph API to get more accurate presences information.
Even if the SBC is never going to have to handle REFER, it may still receive a re-INVITE when the original call is put on hold. You should therefore make sure that your core servers or gateways are able to play music-on-hold to both ends of the call during a transfer, even if the transfer itself is handled internally within Teams.
And finally
There is only one other “gotcha” I know of. Version 2.4.7 of OpenSIPS (and some early v3 releases) have a bug that means RR parameters are not always added when you call the function add_rr_param(). That bug was fixed so if you are using the latest stable release of v2.4 or v3.1 then you should have no problem here.
Here are some links to Microsoft documentation that I found useful. The last one is a tool that runs under Powershell and provides a handy graphical user interface to some of the direct routing data:
https://docs.microsoft.com/en-us/microsoftteams/direct-routing-voice-routing
https://docs.microsoft.com/en-us/microsoftteams/direct-routing-sbc-multiple-tenants
https://docs.microsoft.com/en-us/microsoftteams/direct-routing-protocols-sip
https://www.myteamslab.com/2019/02/microsoft-teams-direct-routing-tool.html
Hi !
First of all, thank you for this wonderful article!
Like many, incoming and outgoing calls work, but call transfers fail, even though I don’t announce REFER as shown here.
Has anything changed recently on the MS Teams side? What can I miss?
Thanks !
Hi Johnny
Sorry for delay. Unexpected domestic issues have taken me out of my usual orbit, preventing access to the web admin.
Since writing that article, my work with Teams integration has ceased so I cannot say if things have changed.
I can only suggest that you work through my article again because there were some quite subtle details in it concerning how call transfers work.
Good luck. John
Great work on this post! Thanks for sharing your experience!
Thanks Michael.
You might recognise one of the issues identified in the article.
😉