Gestire un servizio SIP con app mobile proprie pone parecchie sfide. Una sfida che si presenta fin dal primo giorno: come si chiama un telefono che non è connesso?
In SIP un dispositivo deve essere registrato prima di poter ricevere qualsiasi cosa, che sia un INVITE o un CANCEL. Registrarsi significa dire al server «sono raggiungibile a questo indirizzo». I sistemi operativi mobile non se ne curano: quando l'app che abbiamo realizzato per SpecialCall passa in background, iOS e Android chiudono presto la sua WebSocket, e la registrazione resta legata a un socket che non esiste più. Se in quel momento mandiamo un INVITE, non risponde nessuno.
La soluzione è nota: inviare al telefono una notifica push, aspettare che l'app si svegli e si registri di nuovo, e solo allora mandare l'INVITE. La parte difficile è l'attesa. In questo articolo spieghiamo come la gestiamo.
Il punto di partenza
All'inizio se ne occupava tutto il nostro backend. Quando doveva far squillare un interprete, inviava lui stesso la notifica push e poi continuava a tentare di avviare la chiamata tramite Asterisk finché il dispositivo non si registrava, rinunciando dopo 60 secondi. Funzionava, ma era un ciclo di polling distribuito su due sistemi. La push e la chiamata non sapevano nulla l'una dell'altra, ogni tentativo costava tempo e il backend doveva conoscere sia lo stato SIP sia APNs/FCM.
Abbiamo quindi cambiato in modo sostanziale la nostra architettura SIP: abbiamo messo un proxy SIP davanti alle app. Un proxy può trattenere un INVITE e aspettare da solo una registrazione. Il ciclo di tentativi non serve più.
OpenSIPS
Il proxy che abbiamo scelto è OpenSIPS, un server SIP open source maturo, in produzione da quasi vent'anni. Non è un centralino come Asterisk: non mixa l'audio, non riproduce messaggi e non collega canali. Il suo compito è instradare i messaggi SIP, e lo fa in modo molto veloce e flessibile.
OpenSIPS si programma con un linguaggio di scripting proprio. Il file di configurazione è un insieme di route, blocchi di logica eseguiti per ogni richiesta, risposta, errore o evento interno. Le funzionalità arrivano da moduli che si caricano secondo necessità: per esempio registrar e usrloc fanno da registrar e mantengono la tabella delle posizioni, tm gestisce le transazioni con stato, rest_client esegue chiamate HTTP e auth_db si occupa dell'autenticazione digest.
Ciò che lo rende adatto a noi è che supporta nativamente l'RFC 8599. L'RFC 8599 definisce come un dispositivo SIP include i propri dati push nel Contact del suo REGISTER, e il modulo registrar interpreta questi parametri. Quando arriva un INVITE per un dispositivo il cui socket non c'è più, OpenSIPS può trattenere l'INVITE, generare un evento perché venga inviata una push e rilasciare l'INVITE appena il dispositivo si registra di nuovo.
Nella nostra configurazione OpenSIPS è anche il registrar. Le app vi si collegano tramite WebSocket sicure (WSS) e lì si registrano, mentre Asterisk passa a OpenSIPS le chiamate destinate a loro attraverso un semplice trunk UDP. In più, OpenSIPS verifica le credenziali delle app direttamente sulla tabella di autenticazione PJSIP di Asterisk, così non abbiamo dovuto migrare nessuna credenziale.
Gorush
OpenSIPS non invia le notifiche push da sé: segnala quando ne serve una e lascia a noi l'invio. Non volevamo scrivere l'ennesimo client APNs/FCM, quindi abbiamo usato un altro progetto open source: Gorush.
Gorush è un server di notifiche push scritto in Go. È un unico binario con un solo endpoint HTTP (POST /api/push), che accetta un elenco di notifiche in JSON e le consegna tramite APNs (iOS) o FCM (Android). Gestisce le connessioni ai provider, le chiavi di autenticazione e i nuovi tentativi, ed espone un controllo di salute e metriche Prometheus.
Gira sulla stessa macchina di OpenSIPS e ascolta solo su 127.0.0.1: gli parla soltanto OpenSIPS. Lo usiamo in modalità sincrona, quindi ogni richiesta HTTP attende la risposta di Apple o Google e restituisce l'esito per ogni token. Così abbiamo una riga di log per chiamata che dice se la push di risveglio è partita.
Come si registrano le app
Le app si registrano con i parametri dell'RFC 8599 nella URI del Contact:
Contact: <sip:interpreter@...;transport=ws; pn-provider=apns; pn-param=<TEAM_ID>.example.voip; pn-prid=<PushKit token>>pn-providervaleapnssu iOS efcmsu Android.pn-paramidentifica l'app: il team APNs e il bundle ID con il suffisso.voip, oppure il progetto Firebase.pn-pridè il token push del dispositivo. Su iOS è il token PushKit VoIP, non quello delle normali notifiche remote: solo le push VoIP possono svegliare un'app completamente chiusa e mostrare la schermata di chiamata nativa tramite CallKit.
Usiamo una scadenza della registrazione di una settimana. Gli interpreti devono restare raggiungibili a lungo anche se non aprono l'app, e quindi non si registrano di nuovo, da giorni. La WebSocket dietro la registrazione sarà morta molto prima che passi la settimana, ma non importa: quello che ci serve conservare è il contatto con i suoi parametri push. Finché OpenSIPS li ha, può svegliare il telefono.
Vita di una chiamata
- SIP
- push / HTTP
- controllo chiamata (ARI)
- INVITE trattenuto
Backend. Decide chi chiamare: gli interpreti che parlano la lingua giusta, sono disponibili e non sono già impegnati in una chiamata. Poi chiede ad Asterisk di avviare una chiamata verso ciascuno di loro.
Asterisk. Crea il canale e invia l'INVITE al trunk di OpenSIPS.
OpenSIPS. Riconosce che l'INVITE è per uno dei suoi utenti registrati e lo cerca nella tabella delle posizioni. Gli esiti possibili sono tre:
- Il dispositivo è connesso. L'INVITE viene inoltrato subito e nessuna push viene inviata.
- Il dispositivo ha un contatto abilitato alle push. Il registrar trattiene l'INVITE e genera un evento
E_UL_CONTACT_REFRESH. La nostra route di evento compone la richiesta per Gorush e la invia con una POST. Quando l'app si registra di nuovo, OpenSIPS rilascia l'INVITE trattenuto sulla nuova connessione. - Il dispositivo non è registrato affatto. Non c'è niente da svegliare, quindi OpenSIPS rifiuta subito l'INVITE e il backend passa oltre.
Gorush. Consegna la notifica tramite APNs o FCM.
App. Si sveglia, mostra la chiamata in arrivo (con CallKit su iOS), si registra e riceve l'INVITE.
Il risultato
Il backend non sa più che le notifiche push esistono. Chiede ad Asterisk di avviare una chiamata e Asterisk invia un INVITE, come se chiamasse un telefono da scrivania. Svegliare il dispositivo, aspettarlo e gestire la corsa con un altro interprete avviene tutto in un unico punto, dentro una sola transazione SIP, con due componenti open source e qualche centinaio di righe di configurazione di OpenSIPS.



