Skip to content

Menu

← Blog

RFC 8599 in practice: SIP push notifications with OpenSIPS and Gorush

How we ring an app that has been closed: OpenSIPS holds the INVITE, Gorush sends the push notification and the call goes through once the phone registers again.

VoIP6 min read
In this article
  1. Where we started
  2. OpenSIPS
  3. Gorush
  4. How the apps register
  5. Life of a call
  6. Result

Running a SIP service with your own mobile apps has plenty of challenges. One shows up on day one: how do you call a phone that isn't connected?

In SIP, a device has to be registered before you can send it anything, whether that's an INVITE or a CANCEL. Registering means telling the server “I'm reachable at this address”. Mobile operating systems don't care about this. When the app we built for SpecialCall goes to the background, iOS and Android soon close its WebSocket, and the registration is left pointing at a socket that no longer exists. If we send an INVITE at that moment, nothing answers.

The fix is well known: send the phone a push notification, wait for the app to wake up and register again, and only then send the INVITE. The hard part is the waiting. This post explains how we do it.

Where we started

At first our backend handled all of this. When it wanted to ring an interpreter, it sent the push notification itself and then kept trying to start the call through Asterisk until the device registered, giving up after 60 seconds. It worked, but it was a polling loop spread across two systems. The push and the call didn't know about each other, every retry cost time, and the backend had to understand both SIP state and APNs/FCM.

So we made a big change to our SIP setup: we put a SIP proxy in front of the apps. A proxy can hold an INVITE and wait for a registration on its own. We no longer need the retry loop.

OpenSIPS

The proxy we chose is OpenSIPS, a mature open source SIP server that has been in production use for close to twenty years. It isn't a PBX like Asterisk. It doesn't mix audio, play prompts or bridge channels. Its job is to route SIP messages, and it does that very fast and with a lot of flexibility.

You program OpenSIPS with its own scripting language. The config file is a set of routes, which are blocks of logic that run for every request, reply, failure or internal event. Features come from modules that you load as needed. For example, registrar and usrloc act as a registrar and keep a location table, tm handles stateful transactions, rest_client makes HTTP calls, and auth_db handles digest authentication.

What made it a good fit for us is that it supports RFC 8599 natively. RFC 8599 defines how a SIP device includes its push details in the Contact of its REGISTER. The registrar module understands those parameters. When an INVITE arrives for a device whose socket is gone, OpenSIPS can hold the INVITE, fire an event so a push gets sent, and release the INVITE once the device registers again.

In our setup OpenSIPS is also the registrar. The apps connect to it over secure WebSockets (WSS) and register there, and Asterisk sends their calls to OpenSIPS over a plain UDP trunk. As a bonus, OpenSIPS checks app credentials directly against Asterisk's PJSIP auth table, so we didn't have to migrate any credentials.

Gorush

OpenSIPS doesn't send push notifications itself. It tells you when one is needed and leaves the sending to you. We didn't want to write another APNs/FCM client, so we used another open source project: Gorush.

Gorush is a push notification server written in Go. It's a single binary with one HTTP endpoint (POST /api/push) that accepts a JSON list of notifications and delivers them through APNs (iOS) or FCM (Android). It handles the provider connections, auth keys and retries, and it exposes a health check and Prometheus metrics.

It runs on the same machine as OpenSIPS and listens only on 127.0.0.1. Only OpenSIPS ever talks to it. We run it in sync mode, so each HTTP request waits until Apple or Google replies and returns the result for every token. That gives us one log line per call saying whether the wake-up push went out.

How the apps register

The apps register with the RFC 8599 parameters on their Contact URI:

REGISTER · Contact
Contact: <sip:interpreter@...;transport=ws; pn-provider=apns; pn-param=<TEAM_ID>.example.voip; pn-prid=<PushKit token>>
  • pn-provider is apns on iOS and fcm on Android.
  • pn-param identifies the app: the APNs team and bundle ID with the .voip suffix, or the Firebase project.
  • pn-prid is the device's push token. On iOS this is the PushKit VoIP token, not the normal remote notification token. Only VoIP pushes can wake an app that is completely closed and show the native call screen through CallKit.

We use a registration expiry of one week. Interpreters need to stay reachable for a long time even if they haven't opened the app, and therefore haven't registered again, in days. The WebSocket behind the registration will be dead long before the week is up. That's fine, because what we need to keep is the contact and its push parameters. As long as OpenSIPS has those, it can wake the phone.

Life of a call

BackendAsteriskOpenSIPSGorushAppasleep · socket closedawakelookup: push contactINVITE heldoriginate (ARI)INVITE · UDPPOST /api/pushAPNs / FCM pushREGISTER · WSSINVITE releasedrings
  • SIP
  • push / HTTP
  • call control (ARI)
  • INVITE held
OpenSIPS holds the INVITE until the app, woken by the push, registers again: then it releases it to the new connection and the phone rings.

Backend. Decides who to call: which interpreters speak the right language, are available and aren't already on a call. Then it asks Asterisk to start a call to each of them.

Asterisk. Creates the channel and sends the INVITE to the OpenSIPS trunk.

OpenSIPS. Sees that the INVITE is for one of its registered users and looks the user up in the location table. There are three possible outcomes:

  • The device is connected. The INVITE is relayed right away and no push is sent.
  • The device has a push-enabled contact. The registrar holds the INVITE and fires an E_UL_CONTACT_REFRESH event. Our event route builds the Gorush request and POSTs it. When the app registers again, OpenSIPS releases the held INVITE to the new connection.
  • The device isn't registered at all. There is nothing to wake up, so OpenSIPS rejects the INVITE right away and the backend moves on.

Gorush. Delivers the notification through APNs or FCM.

App. Wakes up, shows the incoming call (CallKit on iOS), registers, and receives the INVITE.

Result

The backend no longer knows push notifications exist. It asks Asterisk to start a call and Asterisk sends an INVITE, the same as calling a desk phone. Waking the device, waiting for it and handling a race with another interpreter all happen in one place, inside a single SIP transaction, using two open source components and a few hundred lines of OpenSIPS config.

VoIP

Want to know more?

Write to us or give us a call: we'll tell you how we work and what we could do for your company.

Our VoIP serviceContact us

More articles

  • VoIP

    Monitoring Asterisk with Prometheus

    Since Asterisk 17, metrics come out in Prometheus format: what they are and how to scrape them, with a small Docker project to try.

    · 4 min
  • Development

    A smarter office, right at your fingertips

    With Home Assistant we control the office's lights, air conditioning, cameras and other devices from one place, phone included.

    · 3 min
  • Helpdesk

    A management system for hardware call-outs across Italy

    The management system we built for hardware support: field engineers, devices, contacts and tickets.

    · 3 min

All articles