Some rough steps for posterity's sake:
Microsoft Side:
Add domain and validate domain
Assign domain to at least 1 user (otherwise you can't add an sbc for that user)
Create sbc under voice->direct routing (once fusionpbx is configured and pinging properly you will need to validate that tls connectivity status and sip options status are Active)
sbc needs to have forward p-id set as well
Create voice route with dialed number pattern .* going to your sbc
Create Dial Plan Normalization rule on global dialplan that maps ^(.*)$ to $1
Use Powershell scripts:
Create NumberTranslation StripPlus
https://docs.microsoft.com/en-us/powershell/module/skype/new-csteamstranslationrule?view=skype-ps
Assign number translation rule to sbc
https://docs.microsoft.com/en-us/microsoftteams/direct-routing-translate-numbers
Get really really frustrated that nothing is working, so reset the Global Calling Policy
Fusionpbx Side:
Configuration:
edit /etc/freeswitch/autoload-configs/sofia.conf.xml
to enable capture server <param name="capture-server" value="udp:127.0.0.1:9060"/>
Troubleshooting without sngrep was pretty much impossible for me.
I use a simple bash script to read my encrypted sngrep now.
#!/bin/sh
fs_cli -x 'sofia global capture on'
sngrep -L udp:127.0.0.1:9060 -d lo
fs_cli -x 'sofia global capture off'
Profile:
Create a new external (or Internal could work as well depending on how you want to play it) sip profile.
set appropriate tls-sip-port and sip-port (I decided to implement a Gateway and a profile for each Microsoft tenant, this may not be necessary)
Set ext-sip-ip and sip-ip to be FQDN of Microsoft Teams direct routing SBC
Configure and set up proper tls certificate and place into tls-cert-dir parameter of new profile (The only cert necessary should be agent.pem, but I created a letsencrypt post renewal-hook that does my own thing.)
agent.pem, dtls-srtp.pem, tls.pem, and wss.pem are all symlinks to all.pem
My script looks like this:
#/bin/bash
cat /etc/letsencrypt/live/example.com/fullchain.pem /etc/letsencrypt/live/example.com/cert.pem /etc/letsencrypt/live/example.com/privkey.pem > /etc/freeswitch/ssl/example.com/all.pem;
chown www-data.www-data -R /etc/freeswitch/ssl/*
/usr/bin/fs_cli -x 'fsctl shutdown elegant restart' > /dev/null 2>&1;
sleep 5;
systemctl restart freeswitch.service; # Freeswitch sucks at restarting elegantly, and doesn't get new keys without a full restart. So, I found that I have to force a restart.
/usr/sbin/service nginx restart;
set tls-verify-depth from 2 to 9
Make sure and
Gateway:
Create three new gateways to microsoft teams sip.pstnhub.microsoft.com sip2.pstnhub.microsoft.com and sip3.pstnhub.microsoft.com
set proxy in gateway ie sip.pstnhub.microsoft.com:5061
set register transport to tls
set profile to the profile I just made
put in a hack in gateway sofia.conf.lua script to slap in contact-in-ping true for these gateways (Mark's recent commit will solve this as well, I tend to lag Mark's code several months)
comment out line in sofia.conf.lua
--table.insert(xml, [[ <param name="contact-params" value="transport=tls"/>]]);
Gateway pinging is unreliable, use SIPP to send your pings, I did this using a simple cron job, however SIPP must be downloaded and compiled with tls support as the built in packages in debian and ubuntu don't include ssl support.
ACL:
add microsoft ranges 52.112.0.0/14 and 52.120.0.0/14 to domains ACL list
reload ACL
Firewall:
create specific inbound firewall rules for new external port allowing Microsoft ranges to connect
TestTLS:
test tls connection using openssl s_client -connect sbc.example.com:yourport-tls1_2
Inbound Route:
sets the domain and routes the call to that destination inside of the domain, sets privacy (if you are getting "Private Number")
Dialplan rule:
This handles getting extension info for outbound calls
Extensions:
Create extension for each Extension you want to run in Microsoft Teams and modify the dialstring to bridge to the gateway created for teams.
Parking:
Set up a parking lot with numbers only so that Teams users can transfer to it and pick up calls out of it and so that calls can actually return to Teams extensions