Shaleen Jain

· 9 min read

Forwarding Calls and Texts Internationally with a VoIP Server and Modem

<p><em>Special thanks to Pramey Singh for feedback and review.</em></p> <!-- On the path to being a [Sovereign Individual][3], being digitally sovereign --> <!-- is equally important. Owning and decoupling your digital identity from your mobile --> <!-- number is a step in that direction. --> <p>As a <a rel="noopener nofollow noreferrer external" target="_blank" href="https://dougantin.com/the-sovereign-individual-what-you-need-to-know-why/">Sovereign Individual</a>, traveling and staying overseas for long periods frequently, switching sim cards/numbers can quickly become a pain. It can be helpful to still retain control over an operational phone number of your home country across geolocations. Doing so lets you forward and make local home calls and be able to receive your bank’s account and OTP related text messages.</p> <p>The solution proposed here isn’t a location dependent one like using <a rel="noopener nofollow noreferrer external" target="_blank" href="https://fi.google.com/about/">Google Fi</a> or relying on expensive international roaming plans. Instead, it gives you ownership and freedom to retain, switch or dispose of your phone numbers at local rates, across political borders.</p> <span id="continue-reading"></span> <p>In the same vein, I recently tweeted about taking control of your messaging platforms to have custody over your chat messages. Helping you maintain a new layer of privacy via isolation as well as improving your quality of life by unifying your chatting platforms. This article though focuses only on voice and text communication.</p> <script>window.twttr = (function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0], t = window.twttr || {}; if (d.getElementById(id)) return t; js = d.createElement(s); js.id = id; js.src = "https://platform.twitter.com/widgets.js"; fjs.parentNode.insertBefore(js, fjs); t._e = []; t.ready = function(f) { t._e.push(f); }; return t; }(document, "script", "twitter-wjs"));</script> <div style="display: flex; justify-content: center;"><blockquote class="twitter-tweet"><a href="https://twitter.com/shalzzj/status/1499689706367643649"></a></blockquote></div> <br/> <!-- toc --> <h2 id="overview"><a class="zola-anchor" href="#overview" aria-label="Anchor link for: overview">🔗</a> Overview</h2> <p>The solution to the above problem is to move onto a <a rel="noopener nofollow noreferrer external" target="_blank" href="https://en.wikipedia.org/wiki/Session_Initiation_Protocol">SIP</a>/<a rel="noopener nofollow noreferrer external" target="_blank" href="https://en.wikipedia.org/wiki/Real-time_Transport_Protocol">RTP</a> stack for all our voice calls by switching to a VOIP service provider and possibly relying on our PBX server (depending on the jurisdiction’s regulation).</p> <p>When we have our complete VOIP stack in place we’ll have a pipeline like this:</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="plain"><span class="giallo-l"><span>PSTN Carrier &lt;-&gt; 3G/4G Dongle &lt;-&gt; Asterisk Server &lt;-&gt; Twilio (VOIP Provider) &lt;-&gt; VOIP Softphone</span></span></code></pre><h2 id="benefits"><a class="zola-anchor" href="#benefits" aria-label="Anchor link for: benefits">🔗</a> Benefits</h2> <p>With this approach, we can redirect traditional PSTN calls over VOIP to us anywhere across the globe. Plus since there’s no longer a SIM card required to be tethered to a single device, we can make and receive calls across multiple devices.</p> <p>This also mitigates the ever-growing threat of SIM swap attacks. Both, physical attacks as the SIM card associated with your number is no longer required to be always in your phone, and operator social engineering attacks since most VOIP providers have a higher security level than most traditional telecom providers.</p> <h2 id="asterisk-pbx-server"><a class="zola-anchor" href="#asterisk-pbx-server" aria-label="Anchor link for: asterisk-pbx-server">🔗</a> Asterisk PBX Server</h2> <p>While the regulation of some countries like the US allows buying a consumer feature level number from a VOIP provider like <a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.twilio.com/">Twilio</a> with unrestricted functionality for calls and SMS/MMS. The feature set starts to degrade drastically from what we are used to when buying local numbers for most other countries.</p> <p>If you are following this guide with a US number or any other country that allows first-class support to VOIP providers then you can skip this section on setting up an Asterisk server.</p> <p>If however you find yourself with a number from a country that restricts VOIP providers from providing full feature-set local numbers then we can circumvent that via a 3G/4G USB dongle with voice calling support. This allows us to then bring up a PBX server connected to the internet with the ability to route traditional PBX calls via the USB dongle holding your own local number SIM card.</p> <p>The first step is to then install an <a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.asterisk.org/">Asterisk</a> server on a machine that can be left running 24/7 for maximum uptime. This guide uses OpenWRT OS and packages in examples but you can use any machine you have lying around including a Raspberry Pi.</p> <p>With an already set up OpenWRT router, you’ll need the following packages to install Asterisk:</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="shellscript"><span class="giallo-l"><span style="color: #268BD2;">opkg</span><span style="color: #2AA198;"> install Asterisk Asterisk-pjsip Asterisk-bridge-simple Asterisk-codec-alaw Asterisk-codec-ulaw Asterisk-res-rtp-Asterisk Asterisk-res-srtp Asterisk-chan-dongle</span></span></code></pre><h3 id="asterisk-chan-dongle"><a class="zola-anchor" href="#asterisk-chan-dongle" aria-label="Anchor link for: asterisk-chan-dongle">🔗</a> Asterisk-chan-dongle</h3> <p>Now comes the tough part of acquiring and setting up a USB dongle. We’re using <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/wdoekes/asterisk-chan-dongle">wdoekes/Asterisk-chan-dongle</a> an Asterisk channel driver for interfacing with the USB dongle from the Asterisk server.</p> <p>The tricky part here is that the channel driver doesn’t work with every 3G/4G USB dongle, only Huawei 3G dongles and a few 4G dongles. And even with the dongles that it does work, not every supported dongle has voice support available or firmware unlocked.</p> <p>The best way is to look at the list of supported dongles on the project’s <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/wdoekes/asterisk-chan-dongle#chan_dongle-channel-driver-for-huawei-umts-cards">README</a> file and try to get your hands on one of them. I bought an E1750 Huawei 3G dongle which you still find in stock in many online stores. If it’s unavailable in your local markets/ e-commerce sites, try searching it on eBay or AliExpress. I was able to get a second-hand one from eBay with SIM and voice support unlocked.</p> <p>Once you have a dongle, insert your SIM card and plug it into the machine you have Asterisk installed. Make sure you have the <code>asterisk-chan-dongle</code> package installed.</p> <p>To configure, we edit the <code>dongle.conf</code> file found in the configuration folder of Asterisk. The configuration folder is defined via the <code>astetcdir</code> option in the main <code>asterisk.conf</code> file and is usually <code>/etc/asterisk/</code>.</p> <p>If there’s no <code>dongle.conf</code> file located in the configuration folder, create one. The default dongle.conf can be found <a rel="noopener nofollow noreferrer external" target="_blank" href="https://github.com/wdoekes/asterisk-chan-dongle/blob/master/etc/dongle.conf">here</a>.</p> <p>Make a note of the <code>context</code> option in <code>dongle.conf</code>, the default value of <code>context</code> is <code>default</code> which we’ll be referencing later. You can change it to something more appropriate like <code>dongle-incoming</code> to make it easier to remember, which is what we’re doing in the below example.</p> <p>The main configuration requirements are specifying the correct audio and data device ports of the dongle.</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="ini"><span class="giallo-l"><span>[dongle0]</span></span> <span class="giallo-l"><span style="color: #859900;">context</span><span> = dongle-incoming</span></span> <span class="giallo-l"><span style="color: #859900;">audio</span><span> = /dev/ttyUSB1 </span><span style="color: #93A1A1;font-style: italic;">; tty port for audio connection; no default value</span></span> <span class="giallo-l"><span style="color: #859900;">data</span><span> = /dev/ttyUSB2 </span><span style="color: #93A1A1;font-style: italic;">; tty port for AT commands; no default value</span></span></code></pre> <p>The exact value here will depend on your dongle and the distribution you’re running. You might need to install the <code>usb_modeswitch</code> package to switch the dongle from the initial CD-ROM/mass storage mode to the serial mode. You should then be able to start the Asterisk server and watch the logs for errors. Make sure there are no dongle-related errors.</p> <p>Note: To enable logging you might need to edit <code>logger.conf</code> to increase the log level. (You can find the <code>messages</code> log file in the <code>astlogdir</code> folder) :</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="plain"><span class="giallo-l"><span>console =&gt; notice,warning,error</span></span> <span class="giallo-l"><span>messages =&gt; notice,warning,error,debug</span></span></code></pre> <p>Check the status of your dongle and network registration via these two commands in the Asterisk console. (You can attach a console to a running Asterisk server via the command <code>asterisk -r</code>)</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="plain"><span class="giallo-l"><span>dongle show devices</span></span> <span class="giallo-l"><span>dongle show device state dongle0</span></span></code></pre> <p>If the output shows the device state as “Free”, then you’re good to go.</p> <h3 id="sms-forwarding-optional"><a class="zola-anchor" href="#sms-forwarding-optional" aria-label="Anchor link for: sms-forwarding-optional">🔗</a> SMS Forwarding (Optional)</h3> <p>The easiest and simplest way to get acquainted with Asterisk and how to configure it is to set up SMS forwarding from our USB 3G dongle to an email ID.</p> <p>We can do so by editing <code>extensions.conf</code>. The file is already well documented via comments (any line preceded by <code>;</code> is a comment and is ignored).</p> <p>Append the following lines at the end of the file to set up SMS forwarding.</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="ini"><span class="giallo-l"><span>[dongle-incoming]</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; sms,1,Verbose(Incoming SMS from </span><span style="color: #2AA198;">&quot;${CALLERID(num)} ${BASE64_DECODE(${SMS_BASE64})}&quot;</span><span>)</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; sms,n,System(echo </span><span style="color: #2AA198;">&quot;To: user@example.com\nSubject: ${CALLERID(num)}\n\n${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${DONGLENAME} - ${CALLERID(num)}: &quot;</span><span> &gt; /tmp/sms.txt)</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; sms,n,System(echo </span><span style="color: #2AA198;">&quot;${BASE64_DECODE(${SMS_BASE64})}&quot;</span><span> &gt;&gt; /tmp/sms.txt)</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; sms,n,System(msmtp -t &lt; /tmp/sms.txt)</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; sms,n,System(rm -f /tmp/sms.txt)</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; sms,n,Hangup()</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; ussd,1,Verbose(Incoming USSD: ${BASE64_DECODE(${USSD_BASE64})})</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; ussd,n,Hangup()</span></span></code></pre> <p>Here <code>dongle-incoming</code> is the name of the context we defined in <code>dongle.conf</code>, and we define extensions within that context for receiving SMS by the way of <code>exten =&gt; sms</code>. You can read more about how <a rel="noopener nofollow noreferrer external" target="_blank" href="https://wiki.asterisk.org/wiki/display/AST/Contexts%2C+Extensions%2C+and+Priorities">Context, Extensions, and Priorities</a> work in Asterisk from their wiki.</p> <p>Once we craft an email message we defer to a sendmail compatible SMTP client to send the email. This example uses <code>msmtp</code> here, you can instead use any other client and configure it accordingly.</p> <p>The above extension configuration calls a few Asterisk built-in apps and functions that you might need to install separately. For OpenWRT install the following packages:</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="plain"><span class="giallo-l"><span>opkg install asterisk-app-system asterisk-app-verbose asterisk-func-base64</span></span></code></pre> <p>If you just want SMS forwarding to your email and don’t care about PSTN voice calls then congratulations you should have SMS-to-email forwarding working at this point. If you want voice call functionality as well, continue reading.</p> <h3 id="configuration"><a class="zola-anchor" href="#configuration" aria-label="Anchor link for: configuration">🔗</a> Configuration</h3> <p>Once we have everything installed and running, it’s time to configure our Asterisk PBX server to do two things:</p> <ol> <li>Set up call extensions so that Asterisk knows how and where to route incoming and outgoing calls</li> <li>Authenticate and interface with Twilio as a BYOC trunk</li> </ol> <p>While we don’t need to use Twilio or a VOIP service provider to act as a proxy for our PBX server, using one adds a level of security and reliability to our setup as we don’t need to whitelist dynamic IP addresses for communicating with a mobile softphone and Twilio’s global presence and edge locations enable much better performance and reliability than just directly talking to our home Asterisk server from across the globe.</p> <p>The configuration shared here are just minimal examples that should work for most cases, you may have to tweak and adjust them to suit your setup and requirements.</p> <p>There are a few files that we’ll need to edit mainly, <code>pjsip.conf</code>, <code>pjsip_wizard.conf</code> and <code>extensions.conf</code></p> <p>The first thing that we need to do is set up a “transport” for Asterisk to use for SIP signaling in the <code>pjsip.conf</code> file:</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="ini"><span class="giallo-l"><span>[transport-udp]</span></span> <span class="giallo-l"><span style="color: #859900;">type</span><span>=transport</span></span> <span class="giallo-l"><span style="color: #859900;">protocol</span><span>=udp</span></span> <span class="giallo-l"><span style="color: #859900;">bind</span><span>=0.0.0.0:5060</span></span></code></pre> <p>The next step is connecting our PBX to Twilio as a Trunk using their BYOC Trunk option. There is no direct guide provided by Twilio on how to set up a BYOC Trunk, especially with Asterisk, but the process is similar to setting up a Twilio Elastic SIP Trunk.</p> <p>Twilio does, fortunately, provide a good enough guide for setting up an Elastic SIP trunk <a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.twilio.com/docs/sip-trunking/sample-configuration#asterisk">here</a>, from where we can adapt the configuration shared in the “Asterisk Provisioning” section to work with the BYOC trunk.</p> <p>For connecting with the Twilio network as a SIP trunk we need to have a user created with the corresponding credentials in addition to a SIP domain and a BYOC trunk domain. We can do so via the Twilio console dashboard, the exact steps for which are described in the “Twilio” section below.</p> <blockquote> <p>Note: The values highlighted in bold in the example configuration will be unique to your environment and needs to be replaced with appropriate values to work correctly.</p> </blockquote> <p><code>pjsip_wizard.conf:</code></p> <pre style="background-color:#fdf6e3;color:#657b83;"> <code><span>[user_defaults](!) </span><span>type = wizard </span><span>endpoint/context = from-twilio </span><span>endpoint/allow = !all,ulaw,alaw </span><span>endpoint/dtmf_mode = rfc4733 </span><span>endpoint/media_encryption = no </span><span>endpoint/media_encryption_optimistic = no </span><span>endpoint/trust_id_inbound = yes </span><span>endpoint/send_pai = yes </span><span>endpoint/language = en </span><span> </span><span>[<b>+919876543210</b>](user_defaults) </span><span>aor/max_contacts = 5 </span><span>endpoint/callerid = <b>Alan Klein <+919876543210></b> </span><span>remote_hosts = <b>byoc.twilio-asteriskpbx.sip.singapore.twilio.com, byoc.twilio-asteriskpbx.sip.tokyo.twilio.com</b> </span><span> </span><span>[trunk_defaults](!) </span><span>type = wizard </span><span>endpoint/transport=transport-udp </span><span>endpoint/allow = !all,ulaw,alaw </span><span>endpoint/trust_id_inbound=no </span><span>endpoint/dtmf_mode=rfc4733 </span><span>endpoint/allow_subscribe = no </span><span>aor/qualify_frequency = 60 </span><span> </span><span>[twilio-apac](trunk_defaults) </span><span>sends_auth = yes </span><span>sends_registrations = yes </span><span>remote_hosts = <b>twilio-asteriskpbx.sip.singapore.twilio.com </b> </span><span>outbound_auth/username = <b style="color: red;">myasteriskpbx </b> </span><span>outbound_auth/password = <b style="color: red;">myasteriskpbxzx11%VzX </b> </span><span>endpoint/context = from-twilio </span><span>aor/qualify_frequency = 60 </span> </code></pre> <p>Next, we set up the extensions required to receive incoming calls from the PSTN network and forward them as a SIP call via the Twilio SIP domain to any registered softphones. And another extension for when we want to make a call from our softphone to then use the dongle for completing the call to a PSTN number.</p> <p><code>extensions.conf</code>:</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="ini"><span class="giallo-l"><span>[dongle-incoming]</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; +919876543210,1,Dial(PJSIP/twilio-apac/sip:+919876543210@twilio-asteriskpbx.sip.twilio.com, ,b(dongle-incoming^outbound^1))</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; outbound,1,Set(JITTERBUFFER(adaptive)=default)</span></span> <span class="giallo-l"><span style="color: #859900;">same</span><span> =&gt; n,Set(AGC(rx)=4000)</span></span> <span class="giallo-l"><span style="color: #859900;">same</span><span> =&gt; n,Return()</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span>[from-twilio]</span></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; _X.,1,Set(JITTERBUFFER(adaptive)=2000,1600,120)</span></span> <span class="giallo-l"><span style="color: #859900;">same</span><span> =&gt; n,Set(AGC(rx)=4000)</span></span> <span class="giallo-l"><span style="color: #859900;">same</span><span> =&gt; n,Dial(Dongle/dongle0/+91${EXTEN})</span></span> <span class="giallo-l"><span style="color: #859900;">same</span><span> =&gt; n,Hangup</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;">exten</span><span> =&gt; _+91X.,1,Set(JITTERBUFFER(adaptive)=2000,1600,120)</span></span> <span class="giallo-l"><span style="color: #859900;">same</span><span> =&gt; n,Set(AGC(rx)=4000)</span></span> <span class="giallo-l"><span style="color: #859900;">same</span><span> =&gt; n,Dial(Dongle/dongle0/${EXTEN})</span></span> <span class="giallo-l"><span style="color: #859900;">same</span><span> =&gt; n,Hangup</span></span></code></pre> <blockquote> <p>Note: Enabling Jitterbuffer and Automatic Gain Control (AGC) may require additional asterisk packages to be installed. On OpenWRT the package names are: <code>asterisk-func-jitterbuffer</code> and <code>asterisk-func-speex</code>.</p> </blockquote> <h3 id="nat-settings-optional"><a class="zola-anchor" href="#nat-settings-optional" aria-label="Anchor link for: nat-settings-optional">🔗</a> NAT settings (optional)</h3> <p>If you’re running your Asterisk server on a private network and is behind Network Address Translation (NAT) then you’ll need to enable additional configuration options for Asterisk’s <code>res_pjsip</code> module to work reliably.</p> <p>The most basic configuration required for networking behind a NAT is specifying the external media address in the transport configuration section of <code>pjsip.conf</code></p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="diff"><span class="giallo-l"><span>[transport-udp]</span></span> <span class="giallo-l"><span>type=transport</span></span> <span class="giallo-l"><span>protocol=udp</span></span> <span class="giallo-l"><span>bind=0.0.0.0:5060</span></span> <span class="giallo-l"><span style="color: #859900;">+; NAT settings</span></span> <span class="giallo-l"><span style="color: #859900;">+local_net = 127.0.0.1/24</span></span> <span class="giallo-l"><span style="color: #859900;">+local_net = 192.168.1.0/24</span></span> <span class="giallo-l"><span style="color: #859900;">+external_media_address = home.example.com ; or a public static IP</span></span> <span class="giallo-l"><span style="color: #859900;">+external_signaling_address = home.example.com ; or a public static IP</span></span> <span class="giallo-l"><span style="color: #859900;">+allow_reload=no</span></span></code></pre> <p>You can find more details on how to configure Asterisk behind a NAT from their <a rel="noopener nofollow noreferrer external" target="_blank" href="https://wiki.asterisk.org/wiki/display/AST/Configuring+res_pjsip+to+work+through+NAT">wiki</a></p> <h2 id="twilio"><a class="zola-anchor" href="#twilio" aria-label="Anchor link for: twilio">🔗</a> Twilio</h2> <p>The last step in our setup is to configure Twilio SIP domain and BYOC trunk from the Twilio console. Twilio has a nice tutorial for setting up a <a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.twilio.com/blog/registering-sip-phone-twilio-inbound-outbound">SIP phone</a> for incoming/outgoing calls directly from Twilio’s SIP domain.</p> <p>We adapt from the tutorial for setting up two SIP domains:</p> <p>The first SIP domain will be set up similar to the one in the tutorial but with a different webhook URL for the “A Call Comes In” configuration option. The Webhook URL can be the URL of a Twilio Function or self-hosted server of the following Nodejs script:</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="javascript"><span class="giallo-l"><span style="color: #93A1A1;font-style: italic;">/* URL Parameters</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;">URL parameters: defaultCountry=[international country code - ISO alpha2]</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;">Feel free to remove any console.log statements.</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;">*/</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;">// Require `PhoneNumberFormat`.</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;">const</span><span style="color: #268BD2;"> PNF</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> require</span><span>(</span><span style="color: #2AA198;">&#39;google-libphonenumber&#39;</span><span>).</span><span style="color: #268BD2;">PhoneNumberFormat</span><span>;</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;">const</span><span style="color: #268BD2;"> url</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> require</span><span>(</span><span style="color: #2AA198;">&#39;url&#39;</span><span>);</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;">// Get an instance of `PhoneNumberUtil`.</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;">const</span><span style="color: #268BD2;"> phoneUtil</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> require</span><span>(</span><span style="color: #2AA198;">&#39;google-libphonenumber&#39;</span><span>).</span><span style="color: #268BD2;">PhoneNumberUtil</span><span>.</span><span style="color: #268BD2;">getInstance</span><span>();</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;">exports</span><span>.</span><span style="color: #268BD2;">handler</span><span style="color: #859900;"> =</span><span style="color: #586E75;font-weight: bold;"> function</span><span>(context, event, callback) {</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> client</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> context</span><span>.</span><span style="color: #268BD2;">getTwilioClient</span><span>();</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> twiml</span><span style="color: #859900;"> = new</span><span style="color: #268BD2;"> Twilio</span><span>.</span><span style="color: #268BD2;">twiml</span><span>.</span><span style="color: #268BD2;">VoiceResponse</span><span>();</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span> { From:</span><span style="color: #268BD2;"> fromNumber</span><span>, To:</span><span style="color: #268BD2;"> toNumber</span><span>, SipDomainSid:</span><span style="color: #268BD2;"> sipDomainSid</span><span> }</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> event</span><span>;</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> mergedAggregatedE164CredentialUsernames</span><span style="color: #859900;"> =</span><span> [];</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> regExNumericSipUri</span><span style="color: #859900;"> =</span><span style="color: #DC322F;"> /</span><span style="color: #859900;">^</span><span style="color: #DC322F;">sip:((</span><span style="color: #CB4B16;">\+</span><span style="color: #DC322F;">)</span><span style="color: #859900;">?</span><span style="color: #CB4B16;">[0-9]</span><span style="color: #859900;">+</span><span style="color: #DC322F;">)@(</span><span style="color: #CB4B16;">.</span><span style="color: #859900;">*</span><span style="color: #DC322F;">)/</span><span>;</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> regAlphaSipUri</span><span style="color: #859900;"> =</span><span style="color: #DC322F;"> /</span><span style="color: #859900;">^</span><span style="color: #DC322F;">sip:((</span><span style="color: #CB4B16;">[a-zA-Z][\w]</span><span style="color: #859900;">+</span><span style="color: #DC322F;">)@(</span><span style="color: #CB4B16;">.</span><span style="color: #859900;">*</span><span style="color: #DC322F;">))/</span><span>;</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Change the defaultCallerId to a phone number in your account</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // and BYOC_SID to your byoc trunks SID.</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> BYOC_SID</span><span style="color: #859900;"> =</span><span style="color: #2AA198;"> &#39;BY6f6abd4dadcf7d0de2ec4f828a167bac&#39;</span><span>;</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> DEFAULT_CALLER_ID</span><span style="color: #859900;"> =</span><span style="color: #2AA198;"> &#39;+919876543210&#39;</span><span>;</span><span style="color: #93A1A1;font-style: italic;"> // Also our BYOC number</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> defaultCountry</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> event</span><span>.</span><span style="color: #268BD2;">defaultCountry</span><span style="color: #859900;"> ||</span><span style="color: #2AA198;"> &#39;US&#39;</span><span>;</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> fromSipCallerId</span><span style="color: #859900;"> =</span><span> (</span><span style="color: #268BD2;">fromNumber</span><span>.</span><span style="color: #268BD2;">match</span><span>(</span><span style="color: #268BD2;">regExNumericSipUri</span><span>)</span></span> <span class="giallo-l"><span style="color: #859900;"> ?</span><span style="color: #268BD2;"> fromNumber</span><span>.</span><span style="color: #268BD2;">match</span><span>(</span><span style="color: #268BD2;">regExNumericSipUri</span><span>)[</span><span style="color: #D33682;">1</span><span>]</span><span style="color: #859900;"> :</span></span> <span class="giallo-l"><span style="color: #268BD2;"> fromNumber</span><span>.</span><span style="color: #268BD2;">match</span><span>(</span><span style="color: #268BD2;">regAlphaSipUri</span><span>)[</span><span style="color: #D33682;">2</span><span>]);</span></span> <span class="giallo-l"><span style="color: #859900;"> if</span><span> (</span><span style="color: #859900;">!</span><span style="color: #268BD2;">toNumber</span><span>.</span><span style="color: #268BD2;">match</span><span>(</span><span style="color: #268BD2;">regExNumericSipUri</span><span>)) {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">&#39;Dialing an alphanumeric SIP User&#39;</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> twiml</span><span>.</span><span style="color: #268BD2;">dial</span><span>({callerId:</span><span style="color: #268BD2;"> fromSipCallerId</span><span>, answerOnBridge:</span><span style="color: #B58900;"> true</span><span>})</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">sip</span><span>(</span><span style="color: #268BD2;">toNumber</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> callback</span><span>(</span><span style="color: #B58900;">null</span><span>,</span><span style="color: #268BD2;"> twiml</span><span>);</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> normalizedFrom</span><span style="color: #859900;"> =</span><span> (</span><span style="color: #268BD2;">fromNumber</span><span>.</span><span style="color: #268BD2;">match</span><span>(</span><span style="color: #268BD2;">regExNumericSipUri</span><span>)</span></span> <span class="giallo-l"><span style="color: #859900;"> ?</span><span style="color: #268BD2;"> fromNumber</span><span>.</span><span style="color: #268BD2;">match</span><span>(</span><span style="color: #268BD2;">regExNumericSipUri</span><span>)[</span><span style="color: #D33682;">1</span><span>]</span><span style="color: #859900;"> :</span><span style="color: #268BD2;"> DEFAULT_CALLER_ID</span><span>);</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> normalizedTo</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> toNumber</span><span>.</span><span style="color: #268BD2;">match</span><span>(</span><span style="color: #268BD2;">regExNumericSipUri</span><span>)[</span><span style="color: #D33682;">1</span><span>];</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> let</span><span style="color: #268BD2;"> sipDomain</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> toNumber</span><span>.</span><span style="color: #268BD2;">match</span><span>(</span><span style="color: #268BD2;">regExNumericSipUri</span><span>)[</span><span style="color: #D33682;">3</span><span>];</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">`Original From Number: ${</span><span style="color: #268BD2;">fromNumber</span><span style="color: #2AA198;">}`</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">`Original To Number: ${</span><span style="color: #268BD2;">toNumber</span><span style="color: #2AA198;">}`</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">`Normalized PSTN From Number: ${</span><span style="color: #268BD2;">normalizedFrom</span><span style="color: #2AA198;">}`</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">`Normalized To Number: ${</span><span style="color: #268BD2;">normalizedTo</span><span style="color: #2AA198;">}`</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">`SIP CallerID: ${</span><span style="color: #268BD2;">fromSipCallerId</span><span style="color: #2AA198;">}`</span><span>);</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Parse number with US country code and keep raw input.</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> rawFromNumber</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> phoneUtil</span><span>.</span><span style="color: #268BD2;">parseAndKeepRawInput</span><span>(</span><span style="color: #268BD2;">normalizedFrom</span><span>,</span><span style="color: #268BD2;"> defaultCountry</span><span>);</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> const</span><span style="color: #268BD2;"> rawtoNumber</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> phoneUtil</span><span>.</span><span style="color: #268BD2;">parseAndKeepRawInput</span><span>(</span><span style="color: #268BD2;">normalizedTo</span><span>,</span><span style="color: #268BD2;"> defaultCountry</span><span>);</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Format number in E.164 format</span></span> <span class="giallo-l"><span style="color: #268BD2;"> fromE164Normalized</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> phoneUtil</span><span>.</span><span style="color: #268BD2;">format</span><span>(</span><span style="color: #268BD2;">rawFromNumber</span><span>,</span><span style="color: #268BD2;"> PNF</span><span>.</span><span style="color: #268BD2;">E164</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> toE164Normalized</span><span style="color: #859900;"> =</span><span style="color: #268BD2;"> phoneUtil</span><span>.</span><span style="color: #268BD2;">format</span><span>(</span><span style="color: #268BD2;">rawtoNumber</span><span>,</span><span style="color: #268BD2;"> PNF</span><span>.</span><span style="color: #268BD2;">E164</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">`E.164 From Number: ${</span><span style="color: #268BD2;">fromE164Normalized</span><span style="color: #2AA198;">}`</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">`E.164 To Number: ${</span><span style="color: #268BD2;">toE164Normalized</span><span style="color: #2AA198;">}`</span><span>);</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> function</span><span style="color: #268BD2;"> enumerateCredentialLists</span><span>(sipDomainSid) {</span></span> <span class="giallo-l"><span style="color: #859900;"> return</span><span style="color: #268BD2;"> client</span><span>.</span><span style="color: #268BD2;">sip</span><span>.</span><span style="color: #268BD2;">domains</span><span>(</span><span style="color: #268BD2;">sipDomainSid</span><span>)</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">auth</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">registrations</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">credentialListMappings</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">list</span><span>();</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span style="color: #586E75;font-weight: bold;"> function</span><span style="color: #268BD2;"> getSIPCredentialListUsernames</span><span>(credList) {</span></span> <span class="giallo-l"><span style="color: #859900;"> return</span><span style="color: #268BD2;"> client</span><span>.</span><span style="color: #268BD2;">sip</span><span>.</span><span style="color: #268BD2;">credentialLists</span><span>(</span><span style="color: #268BD2;">credList</span><span>)</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">credentials</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">list</span><span>();</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span style="color: #268BD2;"> enumerateCredentialLists</span><span>(</span><span style="color: #268BD2;">sipDomainSid</span><span>).</span><span style="color: #268BD2;">then</span><span>(credentialLists</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span> {</span></span> <span class="giallo-l"><span style="color: #859900;"> Promise</span><span>.</span><span style="color: #268BD2;">all</span><span>(</span><span style="color: #268BD2;">credentialLists</span><span>.</span><span style="color: #268BD2;">map</span><span>(credList</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span> {</span></span> <span class="giallo-l"><span style="color: #859900;"> return</span><span style="color: #268BD2;"> getSIPCredentialListUsernames</span><span>(</span><span style="color: #268BD2;">credList</span><span>.</span><span style="color: #268BD2;">sid</span><span>);</span></span> <span class="giallo-l"><span> }))</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">then</span><span>(results</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span> {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> results</span><span>.</span><span style="color: #268BD2;">forEach</span><span>(credentials</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span> {</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // Merge together all SIP Domain associated registration</span></span> <span class="giallo-l"><span style="color: #93A1A1;font-style: italic;"> // credential list usernames prefixed by + into one array</span></span> <span class="giallo-l"><span style="color: #268BD2;"> mergedAggregatedE164CredentialUsernames</span><span>.</span><span style="color: #268BD2;">push</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">apply</span><span>(</span><span style="color: #268BD2;">mergedAggregatedE164CredentialUsernames</span><span>,</span></span> <span class="giallo-l"><span style="color: #268BD2;"> credentials</span><span>.</span><span style="color: #268BD2;">filter</span><span>(record</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span style="color: #268BD2;"> record</span><span>[</span><span style="color: #2AA198;">&quot;username&quot;</span><span>].</span><span style="color: #268BD2;">startsWith</span><span>(</span><span style="color: #2AA198;">&#39;+&#39;</span><span>))</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">map</span><span>(record</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span style="color: #268BD2;"> record</span><span>.</span><span style="color: #268BD2;">username</span><span>));</span></span> <span class="giallo-l"><span> });</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #268BD2;">mergedAggregatedE164CredentialUsernames</span><span>);</span></span> <span class="giallo-l"></span> <span class="giallo-l"><span style="color: #859900;"> if</span><span> (</span><span style="color: #268BD2;">mergedAggregatedE164CredentialUsernames</span><span>.</span><span style="color: #268BD2;">includes</span><span>(</span><span style="color: #268BD2;">toE164Normalized</span><span>)) {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">&#39;Dialing another E.164 SIP User&#39;</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> twiml</span><span>.</span><span style="color: #268BD2;">dial</span><span>({</span></span> <span class="giallo-l"><span> callerId:</span><span style="color: #268BD2;"> fromSipCallerId</span><span>,</span></span> <span class="giallo-l"><span> answerOnBridge:</span><span style="color: #B58900;"> true</span></span> <span class="giallo-l"><span> })</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">sip</span><span>(</span><span style="color: #2AA198;">`sip:${</span><span style="color: #268BD2;">toE164Normalized</span><span style="color: #2AA198;">}@${</span><span style="color: #268BD2;">sipDomain</span><span style="color: #2AA198;">}`</span><span>);</span></span> <span class="giallo-l"><span> }</span><span style="color: #859900;"> else if</span><span> (</span><span style="color: #268BD2;">fromE164Normalized</span><span style="color: #859900;"> ===</span><span style="color: #268BD2;"> DEFAULT_CALLER_ID</span><span>) {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">&#39;Dialing a PSTN Number via BYOC&#39;</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> twiml</span><span>.</span><span style="color: #268BD2;">dial</span><span>({callerId:</span><span style="color: #268BD2;"> fromE164Normalized</span><span>, answerOnBridge:</span><span style="color: #B58900;"> true</span><span>})</span></span> <span class="giallo-l"><span> .</span><span style="color: #268BD2;">number</span><span>({byoc:</span><span style="color: #268BD2;"> BYOC_SID</span><span>},</span><span style="color: #268BD2;"> toE164Normalized</span><span>);</span></span> <span class="giallo-l"><span> }</span><span style="color: #859900;"> else</span><span> {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #2AA198;">&#39;Dialing a PSTN Number&#39;</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> twiml</span><span>.</span><span style="color: #268BD2;">dial</span><span>(</span></span> <span class="giallo-l"><span> {callerId:</span><span style="color: #268BD2;"> fromE164Normalized</span><span>, answerOnBridge:</span><span style="color: #B58900;"> true</span><span>},</span></span> <span class="giallo-l"><span style="color: #268BD2;"> toE164Normalized</span></span> <span class="giallo-l"><span> );</span></span> <span class="giallo-l"><span> }</span></span> <span class="giallo-l"><span style="color: #268BD2;"> callback</span><span>(</span><span style="color: #B58900;">null</span><span>,</span><span style="color: #268BD2;"> twiml</span><span>);</span></span> <span class="giallo-l"><span> }).</span><span style="color: #268BD2;">catch</span><span>(err</span><span style="color: #586E75;font-weight: bold;"> =&gt;</span><span> {</span></span> <span class="giallo-l"><span style="color: #268BD2;"> console</span><span>.</span><span style="color: #268BD2;">log</span><span>(</span><span style="color: #268BD2;">err</span><span>);</span></span> <span class="giallo-l"><span style="color: #268BD2;"> callback</span><span>(</span><span style="color: #268BD2;">err</span><span>);</span></span> <span class="giallo-l"><span> });</span></span> <span class="giallo-l"><span> });</span></span> <span class="giallo-l"><span>};</span></span></code></pre> <p>Change the <code>DEFAULT_CALLER_ID</code> variable value to your SIM card number and <code>BYOC_SID</code> value to the SID you get after creating a BYOC trunk SIP domain described below.</p> <p>The above script either calls a PSTN number via our BYOC trunk when calling from our BYOC number or via a Twilio number when calling from a username formatted as an E.164 Twilio number or makes a SIP call when the To number is a username in our credentials list.</p> <p>The second SIP domain will have the selection “BYOC trunk” as the configuration under the “Call Control Configuration” section. The details for setting up a Twilio BYOC trunk can be found in Twilio’s <a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.twilio.com/docs/voice/bring-your-own-carrier-byoc">BYOC trunk</a> documentation.</p> <p>You can also enable voicemail on incoming calls to your number by adding an <code>action</code> field to the <code>&lt;Dial&gt;</code> verb followed by the <code>&lt;Sip&gt;</code> noun</p> <pre class="giallo" style="color: #657B83; background-color: #FDF6E3;" ><code data-lang="diff"><span class="giallo-l"><span style="color: #268BD2;font-style: italic;">@@ -77,6 +77,7 @@</span><span> exports.handler = function(context, event, callback) {</span></span> <span class="giallo-l"><span> if (mergedAggregatedE164CredentialUsernames.includes(toE164Normalized)) {</span></span> <span class="giallo-l"><span> console.log(&#39;Dialing another E.164 SIP User&#39;);</span></span> <span class="giallo-l"><span> twiml.dial({</span></span> <span class="giallo-l"><span style="color: #859900;">+ action: url.resolve(context.PATH, &#39;voicemail&#39;),</span></span> <span class="giallo-l"><span> callerId: fromSipCallerId,</span></span> <span class="giallo-l"><span> record: &quot;record-from-answer&quot;,</span></span> <span class="giallo-l"><span> answerOnBridge: true</span></span> <span class="giallo-l"></span></code></pre> <p>You then need to have a <code>/voicemail</code> webhook available which then records a message. Here’s an example: <a rel="noopener nofollow noreferrer external" target="_blank" href="https://gist.github.com/shalzz/3046edd4d2dca123875ac84853f1cbc1">https://gist.github.com/shalzz/3046edd4d2dca123875ac84853f1cbc1</a></p> <p>Twilio provides a generous amount of trial credits, letting you test and correct your setup before moving onto a paid plan which is when you start paying for usage. You can use my <a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.twilio.com/referral/KrXNxn">Twilio referral link</a> to get $10 in credit when you upgrade.</p> <h2 id="softphone"><a class="zola-anchor" href="#softphone" aria-label="Anchor link for: softphone">🔗</a> Softphone</h2> <p>The last remaining piece of the puzzle is a softphone app through which you’ll receive and make calls. There are a few good softphone apps that work across platforms:</p> <ul> <li><a rel="noopener nofollow noreferrer external" target="_blank" href="https://linphone.org/">Linephone</a></li> <li><a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.acrobits.net/sip-client-ios-android/">Acrobits Softphone</a></li> <li><a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.counterpath.com/bria-solo/">Bria</a></li> </ul> <h2 id="further-improvements"><a class="zola-anchor" href="#further-improvements" aria-label="Anchor link for: further-improvements">🔗</a> Further Improvements</h2> <ul> <li><a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.twilio.com/docs/voice/api/secure-media">SRTP Media transport</a></li> <li><a rel="noopener nofollow noreferrer external" target="_blank" href="https://www.twilio.com/docs/global-infrastructure/understanding-twilio-regions">Twilio region</a></li> </ul>
Liked this article? Share this with others

Got any questions or comments? Drop me a message on Twitter @shalzzj

Sign up for my newsletter to be the first to know about a new post