Microsoft Teams, Direct Routing and Asterisk running on a Raspberry Pi 4
Thanks to another post for assisting with my knowledge of setting up Asterisk as a gateway to Microsoft Teams. (This is not supported but a good cost effective way to learn how Microsoft Direct Routing works). A Raspberry Pi 4 was the smallest footprint to run a gateway for Teams without having virtual infrastructure, appliances etc while learning and doing a proof of concept just to test some Poly handsets etc.
Link to post that got me started:
https://www.ayonik.de/blog/item/90-microsoft-teams-direct-routing-with-asterisk-pbx
Thanks to ayonik.de for getting my POC started with their notes. I now have a fully working Asterisk to Direct Routing MS Teams trunk. I only use it for POC/demos at work but if you are interested in learning more about Asterisk and Microsoft Teams it is a good place to start. I know very little about Asterisk so I have only put together a config just to be able to test. I can share my config and learnings in the hope of helping someone else out. Only abbreviated notes but happy to help if you are stuck. Also happy to take advice on how I could do it better. I'm not looking to boil the ocean.
Topology:
Poly D230 Dect Cordless IP Handset ==>Asterisk==>Direct Routing==>MS TEams==>Poly CCX 500 Teams Handset or Laptop(s)
So pretending D230 Dect is on the PSTN with e.g.+61755551000 and MS TEams Handset/Laptop has Dummy +61755559000. Testing Call forking inbound to ring another D230 and MS Tams at the same time. and vicy versa.
Recompiled Asterisk (first on Asterisk 17.0.1 but now on 17.3 due to intermittent / dodgy failing on refer on transfer with SIP). So I would start with Asterisk 17.3 and recompile with headers that match your DNS name for the Asterisk "SBC" (using term loosely) to Microsoft Teams direct routing trunk.
Make sure you have a valid cert. I use LetsEncrypt 90 day certs as they are free for testing.
Setup a trial or test O365 tenant with E5 licenses etc if you can get it.
Setup DNS correctly. My SIP SBC is sipconnect.o365.fullys.xyz
Powershell:
Setup Powershell first
Install Skype For Business online module
If the Skype for Business Online Windows PowerShell Module is not already installed on the current Windows computer then download and install it from here: https://www.microsoft.com/en-us/download/details.aspx?id=39366
Make sure the powershell script can run:
Set-ExecutionPolicy Unrestricted
Setup powershell session for the Teams commands:
Import-Module SkypeOnlineConnector
$sfbo = New-CsOnlineSession -UserName "afullagar@o365.fullys.xyz"
Import-PSSession $sfbo
Disbale CS-IpPhonePolicy just to make sure devices are not updated when least expected.
Set-CsIPPhonePolicy -EnableDeviceUpdate $false
Run Powershell for the Direct Routing trunk:
New-CsOnlinePSTNGateway -Fqdn sipconnect.o365.fullys.xyz -SipSignallingPort 5061 -MaxConcurrentSessions 10 -Enabled $true
There are other powershell commands to run like dialplan and Set-CsUser settings etc.
Set Teams Calling Policy
Check if TeamsCalling is enabled for users
$userName="eg1@o365.fullys.xyz"
Grant-CsTeamsCallingPolicy -PolicyName Tag:AllowCalling -Identity $userName
Get-CsOnlineUser | ft display*,userprin*,teamscallingpolicy
e.g.
DisplayName UserPrincipalName TeamsCallingPolicy
----------- ----------------- ------------------
eg 1 eg1@o365.fullys.xyz AllowCalling
eg 2 eg2@o365.fullys.xyz AllowCalling
eg 3 eg3@o365.fullys.xyz AllowCalling
Add a test PSTN Usage and voice route for all.
Set-CsOnlinePstnUsage -Identity Global -Usage @{Add="All"}
PS C:\Windows\system32> Get-CsOnlinePstnUsage
Identity : Global
Usage : {All}
New-CsOnlineVoiceRoute -Identity "All" -NumberPattern ".*" -OnlinePstnGatewayList sipconnect.o365.fullys.xyz -Priority 0 -OnlinePstnUsages "All"
PS C:\Windows\system32> Get-CsOnlineVoiceRoute
Identity : All
Priority : 0
Description :
NumberPattern : .*
OnlinePstnUsages : {All}
OnlinePstnGatewayList : {sipconnect.o365.fullys.xyz}
Name : All
Identity : LocalRoute
Priority : 1
Description :
NumberPattern : ^(\+1[0-9]{10})$
OnlinePstnUsages : {}
OnlinePstnGatewayList : {}
Name : LocalRoute
Identity : Eg-Extensions
Priority : 2
Description :
NumberPattern : ^\+1(\d{3})$
OnlinePstnUsages : {All}
OnlinePstnGatewayList : {sipconnect.o365.fullys.xyz}
Name : Poly-Extensions
Enable users for Ent Voice and assign a number for PSTN
$userName="eg1@o365.fullys.xyz"<br>Set-CsUser -Identity $userName -EnterpriseVoiceEnabled $true -HostedVoiceMail $true -OnPremLineURI tel:+61755559001
Test-CsEffectiveTenantDialPlan -DialedNumber 1000 -Identity afullagar@o365.fullys.xyz
RunspaceId : 3f82d10f-5ce7-4df2-9638-a7c717e94a62
TranslatedNumber : +61755551000
MatchingRule : Description=;Pattern=^(1\d{3})$;Translation=+6175555$1;Name=Test_Extensions;
IsInternalExtension=False
Edit pjsip.conf (Here is mine - may look weird to some seasoned Asterisk pros but it works) - Not all these settings seem to impact the trunk but you can play and see.
[global]
max_forwards=10
user_agent=FullysPBX
default_from_user=fullyspbx ; When Asterisk generates an outgoing SIP request, the
; From header username will be set to this value if
; there is no better option (such as CallerID or
; endpoint/from_user) to be used
default_realm=fullyspbx ; When Asterisk generates a challenge, the digest realm
; will be set to this value if there is no better option
; (such as auth/realm) to be used.
ignore_uri_user_options=yes ; Enable/Disable ignoring SIP URI user field options.
; If you have this option enabled and there are semicolons
; in the user field of a SIP URI then the field is truncated
; at the first semicolon. This effectively makes the semicolon
; a non-usable character for PJSIP endpoint names, extensions,
; and AORs. This can be useful for improving compatability with
; an ITSP that likes to use user options for whatever reason.
; Example:
; URI: "sip:1235557890;phone-context=national@x.x.x.x;user=phone"
; The user field is "1235557890;phone-context=national"
; Which becomes this: "1235557890"
;
; Note: The caller-id and redirecting number strings obtained
; from incoming SIP URI user fields are always truncated at the
; first semicolon.
;===============TRANSPORT
[simpletrans]
type=transport
protocol=udp
bind=10.88.10.5
;===============ENDPOINT TEMPLATES
[endpoint-basic](!)
type=endpoint
context=internal
disallow=all
allow=ulaw,alaw
;direct_media=no
;rtp_symmetric=yes
;rewrite_contact=yes
;sdp_owner=- ; String placed as the username portion of an SDP origin o line
; (default: "-")
sdp_session=FullysPBX ; String used for the SDP session s line (default:
; "Asterisk")
[auth-userpass](!)
type=auth
auth_type=userpass
[aor-single-reg](!)
type=aor
max_contacts=10
;===============EXTENSION 1000
[1000](endpoint-basic)
auth=auth1000
aors=1000
[auth1000](auth-userpass)
password=blahblah
username=1000
[1000](aor-single-reg)
;===============EXTENSION 1001
[1001](endpoint-basic)
auth=auth1001
aors=1001
[auth1001](auth-userpass)
password=blahblah
username=1001
[1001](aor-single-reg)
;===============EXTENSION 1002
[1002](endpoint-basic)
auth=auth1002
aors=1002
[auth1002](auth-userpass)
password=blahblah
username=1002
[1002](aor-single-reg)
;
;===============EXTENSION 1003
[1003](endpoint-basic)
auth=auth1003
aors=1003
[auth1003](auth-userpass)
password=blahblah
username=1003
[1003](aor-single-reg)
;
;===============EXTENSION 1005
;
[1005](endpoint-basic)
auth=auth1005
aors=1005
;
[auth1005](auth-userpass)
password=blahblah
username=1005
;
[1005](aor-single-reg)
;
;===============EXTENSION 1010
;
[1010](endpoint-basic)
auth=auth1010
aors=1010
;
[auth1010](auth-userpass)
password=blahblah
username=1010
;
[1010](aor-single-reg)
;
;
;
[transporttls]
type=transport
protocol=tls
bind=0.0.0.0:5061
cert_file=/etc/asterisk/ssl/cert2.pem
priv_key_file=/etc/asterisk/ssl/privkey2.pem
cipher=ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-RSA-AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-RSA-AES128-SHA,AES256-SHA,AES128-SHA
;method=tlsv1.1
method=sslv23
local_net=10.88.10.0/24,192.168.90.0/24,192.168.74.0/24,192.168.99.0/24,192.168.75.0/24
external_media_address=sipconnect.o365.fullys.xyz
external_signaling_address=sipconnect.o365.fullys.xyz
;qualify=no
;qualify_frequency=60
;contact=sip:sip.pstnhub.microsoft.com
;qualify_timeout=5.0 ; Qualify timeout in fractional seconds (default: "3.0")
;authenticate_qualify=no
;
[msteams_trunk_from_teams]
type=endpoint
transport=transporttls
disallow=all
;allow=silk16,silk8,ulaw,alaw
allow=ulaw,alaw
aors=aor_msteams_trunk_from_teams
media_encryption=sdes
from_domain=sipconnect.o365.fullys.xyz
;sdp_owner=- ; String placed as the username portion of an SDP origin o line
; (default: "-")
sdp_session=FullysPBX ; String used for the SDP session s line (default:
; "Asterisk")
allow_transfer=yes ;testing if it helps teams
[aor_msteams_trunk_from_teams]
type = aor
qualify_frequency=60
contact=sip:sip.pstnhub.microsoft.com
;qualify_timeout=5.0 ; Qualify timeout in fractional seconds (default: "3.0")
;authenticate_qualify=no
[msteams_trunk]
type=endpoint
transport=transporttls
context=msteams
disallow=all
;allow=silk16,silk8,ulaw,alaw
allow=ulaw,alaw
media_encryption=sdes
;qualify=yes
send_pai=no
rewrite_contact=no
;sdp_owner=- ; String placed as the username portion of an SDP origin o line
; (default: "-")
sdp_session=FullysPBX ; String used for the SDP session s line (default:
; "Asterisk")
allow_transfer=yes ;testing if it helps teams
[ident_msteams_trunk]
type=identify
endpoint=msteams_trunk
match=sip-all.pstnhub.microsoft.com
Edit /etc/asterisk/extensions.conf
cat /etc/asterisk/extensions.conf
; extensions.conf - the Asterisk dial plan
;
; Static extension configuration file, used by
; the pbx_config module. This is where you configure all your
; inbound and outbound calls in Asterisk.
;
; This configuration file is reloaded
; - With the "dialplan reload" command in the CLI
; - With the "reload" command (that reloads everything) in the CLI
;
; The "General" category is for certain variables.
;
[general]
;
; If static is set to no, or omitted, then the pbx_config will rewrite
; this file when extensions are modified. Remember that all comments
; made in the file will be lost when that happens.
;
; XXX Not yet implemented XXX
;
static=yes
;
; if static=yes and writeprotect=no, you can save dialplan by
; CLI command "dialplan save" too
;
writeprotect=no
;
; If autofallthrough is set, then if an extension runs out of
; things to do, it will terminate the call with BUSY, CONGESTION
; or HANGUP depending on Asterisk's best guess. This is the default.
;
; If autofallthrough is not set, then if an extension runs out of
; things to do, Asterisk will wait for a new extension to be dialed
; (this is the original behavior of Asterisk 1.0 and earlier).
;
;autofallthrough=no
;
;
;
[internal]
;;;call forking inbound numbers to D230 and MSTEams 9000
;;;exten => 1000,1,Dial(PJSIP/1000&PJSIP/msteams_trunk/sip:+61755559000@sip.pstnhub.microsoft.com:5061)
;Non-Call Forking when testing
exten => 1000,1,Dial(PJSIP/1000)
exten => 1001,1,Dial(PJSIP/1001)
exten => 1003,1,Dial(PJSIP/1003)
exten => 1005,1,Dial(PJSIP/1005)
exten => 1010,1,Dial(PJSIP/1010)
;
exten => 9000,1,Goto(internal,+61755559000,1)
exten => 9001,1,Goto(internal,+61755559001,1)
exten => 9002,1,Goto(internal,+61755559002,1)
exten => 9003,1,Goto(internal,+61755559003,1)
exten => 9005,1,Goto(internal,+61755559005,1)
exten => +61755559000,1,Dial(PJSIP/msteams_trunk/sip:${EXTEN:1}@sip.pstnhub.microsoft.com:5061)
exten => +61755559001,1,Dial(PJSIP/msteams_trunk/sip:${EXTEN:1}@sip.pstnhub.microsoft.com:5061)
exten => +61755559002,1,Dial(PJSIP/msteams_trunk/sip:${EXTEN:1}@sip.pstnhub.microsoft.com:5061)
exten => +61755559003,1,Dial(PJSIP/msteams_trunk/sip:${EXTEN:1}@sip.pstnhub.microsoft.com:5061)
exten => +61755559004,1,Dial(PJSIP/msteams_trunk/sip:${EXTEN:1}@sip.pstnhub.microsoft.com:5061)
exten => +61755559005,1,Dial(PJSIP/msteams_trunk/sip:${EXTEN:1}@sip.pstnhub.microsoft.com:5061)
;
;To allow Remote Attended Transfers
exten => external_replaces,1,NoOp()
exten => s,1,Dial(PJSIP/msteams_trunk/${SIPREFERTOHDR})
;
[msteams]
; to react on incoming SIP Options Packates with an "200 OK"
; Without this MSTeams portal shows SIP OPTIONS Errors
exten => msteams_trunk_from_teams,1,HangUp()
;
exten => +1000,1,Goto(internal,1000,1)
exten => +1001,1,Goto(internal,1001,1)
exten => +611001,1,Goto(internal,1001,1)
;from MS Teams dialing 1000 translated by teams to +617.....
exten => +61755551000,1,Goto(internal,1000,1)
exten => +61755551001,1,Goto(internal,1001,1)
exten => +61755551003,1,Goto(internal,1003,1)
exten => +1003,1,Goto(internal,1003,1)
exten => +1005,1,Goto(internal,1005,1)
exten => +1010,1,Goto(internal,1010,1)
;To allow Remote Attended Transfers - Replace REFER-TO for teams
exten => external_replaces,1,NoOp()
exten => s,1,Dial(PJSIP/msteams_trunk/${SIPREFERTOHDR})
Note: No NAT in between Asterisk and Poly D230 sip handset. Asterisk on Raspberry pi4 (on the NBN Gateway to internet). Using Nftables as much easier so reach out if stuck on that front.
Debug commands
sudo asterisk -vvvvvr
pjsip set history on
core set debug 5
core set verbose 5
pjsip set logger on
pjsip show history
pjsip show history entry 56
e.g.
rp-fw-01*CLI> pjsip show history
No. Timestamp (Dir) Address SIP Message
===== ========== ============================== ===================================
00000 1584889159 * ==> 52.114.14.70:5061 OPTIONS sip:sip.pstnhub.microsoft.com SIP/2.0
00001 1584889160 * <== 52.114.14.70:5061 SIP/2.0 200 OK 00002 1584889160 * <== 52.114.14.70:7177 OPTIONS sip:msteams_trunk_from_teams@sipconnect.o365.fullys.xyz:5061;transport=TLS SIP/2.0 00003 1584889160 * ==> 52.114.14.70:7177 SIP/2.0 200 OK
00004 1584889173 * <== 192.168.90.128:5060 INVITE sip:9000@10.88.10.5:5060 SIP/2.0 00005 1584889173 * ==> 192.168.90.128:5060 SIP/2.0 401 Unauthorized
00006 1584889173 * <== 192.168.90.128:5060 ACK sip:9000@10.88.10.5:5060 SIP/2.0 00007 1584889173 * <== 192.168.90.128:5060 INVITE sip:9000@10.88.10.5:5060 SIP/2.0 00008 1584889173 * ==> 192.168.90.128:5060 SIP/2.0 100 Trying
00009 1584889173 * ==> 52.114.14.70:5061 INVITE sip:61755559000@sip.pstnhub.microsoft.com:5061 SIP/2.0
00010 1584889173 * <== 52.114.14.70:5061 SIP/2.0 100 Trying 00011 1584889173 * <== 52.114.14.70:5061 SIP/2.0 400 Bad Request 00012 1584889173 * ==> 52.114.14.70:5061 ACK sip:61755559000@sip.pstnhub.microsoft.com:5061 SIP/2.0
00013 1584889173 * ==> 192.168.90.128:5060 SIP/2.0 503 Service Unavailable
00014 1584889173 * <== 192.168.90.128:5060 ACK sip:9000@10.88.10.5:5060 SIP/2.0 00015 1584889216 * <== 192.168.90.128:5060 INVITE sip:9000@10.88.10.5:5060 SIP/2.0 00016 1584889216 * ==> 192.168.90.128:5060 SIP/2.0 401 Unauthorized
00017 1584889216 * <== 192.168.90.128:5060 ACK sip:9000@10.88.10.5:5060 SIP/2.0 00018 1584889216 * <== 192.168.90.128:5060 INVITE sip:9000@10.88.10.5:5060 SIP/2.0 00019 1584889216 * ==> 192.168.90.128:5060 SIP/2.0 100 Trying
00020 1584889216 * ==> 52.114.14.70:5061 INVITE sip:61755559000@sip.pstnhub.microsoft.com:5061 SIP/2.0
00021 1584889216 * <== 52.114.14.70:5061 SIP/2.0 100 Trying 00022 1584889216 * <== 52.114.14.70:5061 SIP/2.0 400 Bad Request 00023 1584889216 * ==> 52.114.14.70:5061 ACK sip:61755559000@sip.pstnhub.microsoft.com:5061 SIP/2.0
00024 1584889216 * ==> 192.168.90.128:5060 SIP/2.0 503 Service Unavailable
00025 1584889216 * <== 192.168.90.128:5060 ACK sip:9000@10.88.10.5:5060 SIP/2.0 00026 1584889219 * ==> 52.114.14.70:5061 OPTIONS sip:sip.pstnhub.microsoft.com SIP/2.0
00027 1584889219 * <== 52.114.14.70:5061 SIP/2.0 200 OK 00028 1584889219 * <== 52.114.14.70:7177 OPTIONS sip:msteams_trunk_from_teams@sipconnect.o365.fullys.xyz:5061;transport=TLS SIP/2.0 00029 1584889219 * ==> 52.114.14.70:7177 SIP/2.0 200 OK
00030 1584889279 * ==> 52.114.14.70:5061 OPTIONS sip:sip.pstnhub.microsoft.com SIP/2.0
00031 1584889279 * <== 52.114.14.70:5061 SIP/2.0 200 OK 00032 1584889279 * <== 52.114.14.70:7177 OPTIONS sip:msteams_trunk_from_teams@sipconnect.o365.fullys.xyz:5061;transport=TLS SIP/2.0 00033 1584889279 * ==> 52.114.14.70:7177 SIP/2.0 200 OK
00034 1584889334 * <== 192.168.90.128:5060 INVITE sip:9000@10.88.10.5:5060 SIP/2.0 00035 1584889334 * ==> 192.168.90.128:5060 SIP/2.0 401 Unauthorized
00036 1584889334 * <== 192.168.90.128:5060 ACK sip:9000@10.88.10.5:5060 SIP/2.0 00037 1584889334 * <== 192.168.90.128:5060 INVITE sip:9000@10.88.10.5:5060 SIP/2.0 00038 1584889334 * ==> 192.168.90.128:5060 SIP/2.0 100 Trying
00039 1584889334 * ==> 52.114.14.70:5061 INVITE sip:61755559000@sip.pstnhub.microsoft.com:5061 SIP/2.0
00040 1584889335 * <== 52.114.14.70:5061 SIP/2.0 100 Trying 00041 1584889335 * <== 52.114.14.70:5061 SIP/2.0 180 Ringing 00042 1584889335 * ==> 192.168.90.128:5060 SIP/2.0 180 Ringing
00043 1584889336 * <== 52.114.14.70:5061 SIP/2.0 180 Ringing 00044 1584889337 * <== 52.114.14.70:5061 SIP/2.0 180 Ringing 00045 1584889337 * <== 52.114.14.70:5061 SIP/2.0 183 Session Progress 00046 1584889337 * ==> 192.168.90.128:5060 SIP/2.0 183 Session Progress
00047 1584889337 * ==> 192.168.90.128:5060 SIP/2.0 183 Session Progress
00048 1584889339 * ==> 52.114.14.70:5061 OPTIONS sip:sip.pstnhub.microsoft.com SIP/2.0
00049 1584889339 * <== 52.114.14.70:5061 SIP/2.0 200 OK 00050 1584889339 * <== 52.114.14.70:7177 OPTIONS sip:msteams_trunk_from_teams@sipconnect.o365.fullys.xyz:5061;transport=TLS SIP/2.0 00051 1584889339 * ==> 52.114.14.70:7177 SIP/2.0 200 OK
00052 1584889344 * <== 52.114.14.70:5061 SIP/2.0 200 OK 00053 1584889344 * ==> 192.168.90.128:5060 SIP/2.0 200 OK
00054 1584889344 * <== 192.168.90.128:5060 ACK sip:10.88.10.5:5060 SIP/2.0 00055 1584889344 * ==> 52.114.14.70:5061 ACK sip:api-du-a-asse.pstnhub.microsoft.com:443;x-i=71112d70-d44a-41d0-82bb-f147f2e7c908;x-c=���d�����q� SIP/2.0
00056 1584889348 * <== 192.168.90.128:5060 BYE sip:10.88.10.5:5060 SIP/2.0 00057 1584889348 * ==> 192.168.90.128:5060 SIP/2.0 200 OK
00058 1584889348 * ==> 52.114.14.70:5061 BYE sip:api-du-a-asse.pstnhub.microsoft.com:443;x-i=71112d70-d44a-41d0-82bb-f147f2e7c908;x-c=���d�����q� SIP/2.0
rp-fw-01*CLI> pjsip show history entry 56
<--- History Entry 56 Received from 192.168.90.128:5060 at 1584889348 --->
BYE sip:10.88.10.5:5060 SIP/2.0
Call-ID: 3959642ea2bf8d91@192.168.90.128
Content-Length: 0
CSeq: 8003 BYE
From: "Andrew Fullagar D230" ;tag=SP15aff86857ffdbea3
Max-Forwards: 70
To: ;tag=cb72d178-4b1f-4d0b-8571-15b0782ff2e7
Via: SIP/2.0/UDP 192.168.90.128:5060;rport=5060;received=192.168.90.128;branch=z9hG4bK-cd7b40a5
Authorization: Digest username="1000", realm="fullyspbx", nonce="1584889334/1a8744b5c6d06caebacf32dcd6964663", uri="sip:10.88.10.5:5060", response="308222908856c2b7be89f5270e3304ef", algorithm=MD5, cnonce="f3ff998052f955b6", opaque="436369ba26a8f8c5", qop=auth, nc=00000002
User-Agent: OBIHAI/VVXD230-7.0.2.7359
X-RTP-Stat: PS=548,OS=94256,PR=241,OR=41452,PL=0,JI=4,DU=4,EN=G711U,DE=G711U
Content-Length: 0