Buenas.
Hace ya cierto tiempo comentamos lo interesantes que son los dispositivos de Ubiquiti, dada su apertura y posibilidades en cuanto a despliegue de software. En aquella ocasión, compilamos Kamailio y sus módulos principales para MIPSEL. Esta vez, nos ha tocado para MIPS.
Sentando las bases de la necesidad: Arquitecturas geodistribuidas
A nivel profesional, los que trabajamos en los mundos VoIP y derivados, nos peleamos habitualmente con proyectos internacionales, en los que hay escenarios variados, con sedes grandes distribuidas y, en ocasiones sedes muy pequeñas y/o road-warriors.
El caso es que si a estos panoramas se añaden políticas de atención a clientes «follow the sun» con colas geodistribuidas / prioridades, así como DDIs regionales, el enfoque técnico suele tender a centralizar los servicios, pero garantizar la supervivencia, aunque sea degradada.
En estos casos, y poniendo sobre la mesa las necesidades de DDIs locales, una de las soluciones -en ausencia de líneas PSTN locales- pasa por optar por ITSP’s y tener todo full SIP, sin ningún tramo ni gestión PSTN. Cuando hablamos de soluciones Cloud en Amazon, OVH o similares, siendo la contratación de PSTN’s imposible, es casi el único camino.
¿Pero cual es el problema habitual? El problema habitual es casi siempre de round trip / jitter. Si tenemos un cliente internacional con mucho peso en Europa, y todo montado en un datacenter Europeo profesional, y tenemos una delegación pequeña en China; cuando contratamos un DDI «Virtual» al ITSP, lo que sucederá muy probablemente, en los casos en los que por definición de lógica de negocio la llamada acabe en China, será lo siguiente:
De este esquema, lo único que está realmente en nuestra mano es el Register(si procede) / Indicación al ITSP para estipular de dónde queremos recibir las llamadas. Una vez que optamos por recibirlas en nuestro Proxy/B2BUA/X, estamos ya «vendidos»: conseguir luego que el terminal remoto tenga audio directo con el ITSP sin hacer el roundtrip completo es una odisea importante en la mayoría de las situaciones
Relays de Audio Locales
En nuestra humilde experiencia, en este tipo de entornos muchas veces se opta por gateways PSTN como comentábamos al principio, siendo el esquema esperado:
Es decir, se despliega un gateway, y se busca siempre cumplir con el SIP+RTP Trapezoid, para evitar que el media realice dos saltos inter-continentales, independientemente de que tengamos un proxy, un b2bua o una combinación de ambos.
Continuando por este camino, partiendo de la base de haber establecido que queremos que sea un SIP Trunk lo que tengamos en la oficina remota / país lejano, el esquema que proponemos pasaría a ser:
Lo que hemos ido viendo a lo largo de estos años es que, para determinados países, la contratación directa del SIP Trunk con un proveedor local tiene por lo general mas garantías de calidad. Otra cosa es que nos guste / o no tener los servicios agrupados en un mayorista tipo DIDWW, Voxbone y verlo todo muy claro así como unificar la gestión.
Montando el escenario con Kamailio y Asterisk
Gestionando los Registers
Es probable que el proveedor local requiera que nos registremos, para que «sepa localizarnos», sobre todo en caso de IP’s dinámicas y tal.
En este caso, si tenemos esta necesidad, podemos optar por:
- Si tenemos un Asterisk en nuestro sistema de telefonía, lanzar el Register desde ahí.
- Lanzar un register periódico desde Kamailio (con UAC Module).
Generalmente, la mejor opción suele ser optar por lanzarlo desde Kamailio. Así, en caso de darse una caída de conectividad con el sistema central, se mantiene dicho Register periódico.
Opción: Uso de UAC en Kamailio
En el caso de Kamailio, la definición de los Registers que hay que gestionar sólo puede hacerse vía DB, así que teniendo en cuenta que nuestro objetivo es el EdgeRouter, para evitar acceso a I/O y el overhead de algo tan potente como MySQL, optamos por SQLITE3.
El primer paso es crear el fichero DB y la tabla:
# sqlite3 /etc/kamailio/kamailio.db sqlite> CREATE TABLE `uacreg` ( ...> `id` int(10) NOT NULL, ...> `l_uuid` varchar(64) NOT NULL DEFAULT '', ...> `l_username` varchar(64) NOT NULL DEFAULT '', ...> `l_domain` varchar(128) NOT NULL DEFAULT '', ...> `r_username` varchar(64) NOT NULL DEFAULT '', ...> `r_domain` varchar(128) NOT NULL DEFAULT '', ...> `realm` varchar(64) NOT NULL DEFAULT '', ...> `auth_username` varchar(64) NOT NULL DEFAULT '', ...> `auth_password` varchar(64) NOT NULL DEFAULT '', ...> `auth_proxy` varchar(64) NOT NULL DEFAULT '', ...> `expires` int(11) NOT NULL DEFAULT '0'); sqlite>
Nota: Es prácticamente igual que si fuera creada con kamdbctl pero quitando el auto_increment en el ID, que parece ser que no lo soporta bien.
A continuación, la configuración del módulo para su DB_URL es tal que:
modparam("uac", "reg_db_url", "sqlite:///etc/kamailio/kamailio.db") modparam("uac", "reg_contact_addr", "192.168.1.2:5060")
Nota: el contact address, será el contact que envíe el módulo en los re-registros periódicos, podemos optar por poner nuestra IP Pública, si la conocemos y puerto nateado. Aunque, sinceramente, lo mejor suele ser optar por dejar la IP Privada, ya que el ITSP que hayamos escogido detectará la situación de NAT y actuará en consecuencia 🙂
Y con esto sería todo 🙂 Para probarlo, añadimos una entrada en la tabla uacreg:
sqlite> insert into uacreg values(1,1,'alice','alicedomain','alice','alicedomain','asterisk','alice','alicesecret','sip:10.10.0.201',120);
Capturando, vemos como Kamailio lanza automáticamente el Register y responde correctamente al desafío de autenticación:
Opción: Uso de Register vía Outbound Proxy en Asterisk
En el caso de Asterisk, sería tan sencillo como:
register => [email protected]:password@IP_DEL_PROXY/XXXX
Añadiendo eso en sip.conf, generamos un register periódico desde Asterisk con username «user», contraseña «password», request domain del Register «myitsp.com», enviado vía «IP_DEL_PROXY» y como contact XXXX. A modo de ejemplo, en este caso con un Asterisk en 10.10.9.45 y en 10.10.0.201 un supuesto Proxy (no activo ahora mismo):
Comentarios sobre ambas opciones
En ambas opciones, quizás nos interese tocar el contact, si el ITSP utiliza el username del contact para meter la llamada (en lugar del DID, también habitual), quizás queramos adaptarlo.
Tráfico saliente
Lado Asterisk
La gestión del tráfico saliente, en el caso de Asterisk, es realmente trivial utilizar un outbound proxy como salida.
Tanto si usamos Realtime como si usamos static definition en sip.conf, el atributo a configurar es «outboundproxy». A modo de ejemplo:
[myitsp] type=peer host=myitsp.con outboundproxy=10.10.0.201 disallow=all allow=alaw
Con esta definición, cualquier llamada que lancemos:
exten => ........ Dial(SIP/myitsp/0034944048182)
Será lanzada con request uri en el INVITE: «[email protected]» pero enviada físicamente a 10.10.0.201
Lado Kamailio
En el Kamailio, prácticamente nos vale la misma configuración default, dado que lo estamos haciendo de la forma oficial y el routing a dominos externos está permitido por defecto:
# dispatch requests to foreign domains route(SIPOUT);
En este ejemplo, hemos cogido el mismo FQDN de pruebas (myitsp.com) y 10.10.9.45 es el B2BUA (Asterisk) y 10.10.1.220 el Kamailio:
Tráfico entrante
Lado Asterisk
De nuevo, en el lado de Asterisk la configuración es casi trivial. Atendiendo a cómo realiza Asterisk el matching, aunque tengamos definido el peer de salida, hay que definirlo también en entrada:
[myitsp-in] type=peer host=10.10.0.201 disallow=all allow=alaw insecure=port,invite context=incoming (...o el que sea)
Y con esa definición sería todo. Cualquier llamada entrante desde el ITSP será enviada al Kamailio, que la mandará a este Asterisk y matcheará con «myitsp-ín». También se se enviará al context «incoming», donde tendremos que darle el tratamiento correspondiente.
Lado Kamailio
En el lado Kamailio, se recibirán las requests iniciales (INVITE) que inician el diálogo. Hay que cerciorarse de validar que vengan del ITSP y de encaminarlas a dónde corresponda, en nuestro caso al B2BUA Asterisk.
Para ello, basándonos en la configuración default que se instala:
[...] # record routing for dialog forming requests (in case they are routed) # - remove preloaded route headers remove_hf("Route"); if (is_method("INVITE|SUBSCRIBE")) record_route(); # account only INVITEs if (is_method("INVITE")) { setflag(FLT_ACC); # do accounting } # dispatch requests to foreign domains route(SIPOUT); [...]
Podemos dejarlo, de forma simple para ir probando con algo así como:
[...] if (is_method("INVITE|SUBSCRIBE")) record_route(); # account only INVITEs if (is_method("INVITE")) { setflag(FLT_ACC); # do accounting } if (is_method("INVITE") && ($si=="10.10.9.45")) { xlog("L_INFO","Routing $rm from $fu (IP:$si:$sp) to $ru\n"); $du="sip:10.10.1.2"; route(SIPOUT); } [...]
Nota: Se podría hacer con variables / más elegante, es únicamente para ilustrar.
Con esta prueba, lanzando una prueba nos queda un flujo tipo:
Gestión del Media
Bueno, hasta aquí hemos llegado y en cuanto a la señalización pura, no habría que resolver grandes desafíos. Lo podríamos complementar con las lógicas de failover que comentamos en su momento para tener un escenario más real. ¿Pero qué pasa con el media? ¿No hemos iniciado esta travesía para lidiar con el media y para evitar eso que decíamos del roundtrip? Efectivamente, así es, y sirva esta parte final de este post para comentar estos aspectos 😉
Antes de nada, pongamos sobre la mesa el comportamiento de los elementos implicados:
Comportamiento de Asterisk esperado
chan_sip (1.8.x)
En el caso de Asterisk, si se cumple que la llamada en sí no se esté grabando y que no tenga que hacer transcoding, que el peer/friend no esté definido con NAT=YES y, por último, que no tenga que escuchar tonos dtmf por rfc2833 para lanzar features por ejemplo => intentará liberar el media path.
Todo esto siempre que los dos interlocutores estén definidos con directmedia=yes o directmedia=update. El comportamiento por defecto es el de generar un reinvite en cuanto se establece el diálogo, cursando únicamente unos pocos paquetes rtp.
Sirva este pantallazo comentado de SNGREP, son su flamante nuevo soporte para RTP para ilustrarlo:
Evitando ReInvites: DirectRTPSetup
Desde tiempos de 1.8.x, existe un parámetro en sip.conf con nombre directrtpsetup comentado en la documentación oficial y en el propio sip.conf de samples:
;directrtpsetup=yes ; Enable the new experimental direct RTP setup. This sets up ; the call directly with media peer-2-peer without re-invites. ; Will not work for video and cases where the callee sends ; RTP payloads and fmtp headers in the 200 OK that does not match the ; callers INVITE. This will also fail if directmedia is enabled when ; the device is actually behind NAT.
Desde nuestra más humilde experiencia, aunque la feature la marquen como «experimental», llevamos ya unos cuantos proyectos de envergadura con muchísimo tráfico PBX Style (grupos de salto, redirecciones, capturas, ….) y la verdad es que no hemos tenido nunca ningún problema activando dicha setting.
CHAN_PJSIP
Con PJSIP el comportamiento esperado es el mismo que con directmedia=yes y sin directrtpsetup (no soportado). Es decir, si apostamos por este channel driver para gestionar nuestra parte SIP, tenemos que si o si lidiar con ReInvites en cada establecimiento de llamada.
RTPPRoxy / RTPEngine
Para este tipo de entornos y desafíos, Kamailio de por sí no basta, requiere de un agente de media, en este caso un Media Proxy / RTP Proxy para poder conectar el audio hacia/desde el ITSP con el servidor central y/o los terminales de la delegación concreta.
¿Cual escogemos? ¿Cómo lo instalamos? ¿Dónde debemos activarlo?
Desde el punto de vista técnico, RTPEngine es el presente y futuro, con packet relaying a nivel de kernel y soporte de muchas features increíbles como la repacketización o el briding DTLS/RTP por ejemplo.
Sin embargo, en nuestro caso nos centraremos en RTPProxy, por estar disponible en los repositorios de Debian para MIPS/MIPSEL de forma oficial y por su facilidad de despliegue en nuestro objetivo final: EdgeRouter de Ubiquiti.
Sea como fuere, lo que si es pseudo-compartido entre ellos es el comportamiento:
- En muchas ocasiones (siempre que hay NAT), el proxy de media no sabe realmente a que puerto / IP tiene que mandar el media.
- Así que generalmente, se señaliza (SDP) contra una IP / Puerto y se espera a recibir tráfico RTP ahí, una vez recibido, se queda «locked» a ese punto y el media se gestiona contra dicha IP/Puerto.
A nivel de configuración con Kamailio, aparte de cargar el módulo y activarle el control (UNIX Socket / UDP Control), es necesario invocarlo para que realice el rewrite del SDP:
- En las requests que contengan SDP (INVITE generalmente).
- En las replys que contengan SDP (200 OK / 183 Session Progress generalmente).
- (on reply route)
Para invocarlo, podemos hacer uso de la función rtpproxy_manage directamente, tanto en la request route como en reply route:
rtpproxy_manage('coraf');
En este caso, para ilustrar los flags usados:
c - flags to change the session-level SDP connection (c=) IP if media-description also includes connection information. o - flags that IP from the origin description (o=) should be also changed. r - flags that IP address in SDP should be trusted. Without this flag, rtpproxy ignores address in the SDP and uses source address of the SIP message as media address which is passed to the RTP proxy. a - flags that UA from which message is received doesn't support symmetric RTP. (automatically sets the 'r' flag) f - instructs rtpproxy to ignore marks inserted by another rtpproxy in transit to indicate that the session is already goes through another proxy. Allows creating a chain of proxies.
Es decir, queremos que se modifique el connection information (en otro caso no tendría sentido nada de esto), así como el origin (no es estrictamente necesario) y, sobre todo, lo mas importante: la opción ‘r’.
Con la opción ‘r’, nos fiamos de lo que indique el SDP. Lo podemos hacer tranquilamente ya que:
- El ITSP señalizará bien su extremo de audio, con una IP pública accesible.
- Asterisk hará tb su parte correctamente, primero señalizándose contra si mismo (locuciones iniciales, colas, etc …) y finalmente señalizando contra el terminal local.
Así como la opción ‘a’, que evita intentar simpre symmetric RTP (a parte de que fuerza obviamente la opción r), dado que no estamos en un escenario de NAT con ventana abierta para un puerto de salida, activando estas dos opciones conseguimos que se libere bien el media path:
Nota: ¿Que sucede con los Re-Invites?
Los ReInvites deben ser gestionados exactamente igual, dado que siempre que sea Asterisk quien señalice el Audio, los SIPEndpoints son accesibles, con lo que no hay riesgo.
Pruebas de Rendimiento con Kamailio + RTPProxy en EdgeRouter Lite
En su momento os hablábamos de EdgeRouter-X. Hoy va a ser lo contrario. En esta ocasión hemos optado directamente por EdgeRouter Lite. Principalmente, hemos tomado esta camino porque por un coste económico razonable (sigue siendo un precio imbatible) tenemos mas espacio, un rootfs intercambiable, así como 2 núcleos MIPS. Por otra parte, es algo mas grande que el EdgeRouter-X, y tiene más apariencia de «sólido» 😉 Además, tenemos en cuenta que luego siempre nos gusta Netflow / captagent y derivados …
root@ubnt:/home/ubnt# cat /proc/cpuinfo | egrep "(proce|model)" processor : 0 cpu model : Cavium Octeon+ V0.1 processor : 1 cpu model : Cavium Octeon+ V0.1
Preparando el entorno
RTPProxy
Como suele ser habitual en EdgeOS, lo primero es habilitar los repos de Debian:
configure set system package repository wheezy components 'main contrib non-free' set system package repository wheezy distribution wheezy set system package repository wheezy url http://http.us.debian.org/debian set system package repository wheezy-security components main set system package repository wheezy-security distribution wheezy/updates set system package repository wheezy-security url http://security.debian.org commit save exit
Una vez hecho esto, lo primero es instalar y configurar RTPProxy:
apt-get install rtpproxy [...] root@ubnt:~# cat /etc/default/rtpproxy CONTROL_SOCK=udp:127.0.0.1:22222 EXTRA_OPTS="-l 10.10.0.153"
Una vez configurado, tenemos RTPProxy listo para dar servicio y ser controlado por el puerto UDP22222 de localhost.
Kamailio
Para instalar Kamailio, el gran Kaian nos lo ha dejado muy cómodo. Tras pasar por la fábrica Jenkins, os lo hemos dejado listo para consumir en el repo público:
configure set system package repository irontec components 'main extra' set system package repository irontec distribution kamailio43-wheezy set system package repository irontec url http://packages.irontec.com/debian commit save exit
y con ello, basta con:
apt-get update && apt-get install kamailio
Resultados
Tras lanzar una batería de pruebas de concurrencia, la esperada tabla resumida de nuestras pruebas:
Es decir, podemos comentar que a partir de los 10 diálogos concurrentes con RTP, empiezan a perder rendimiento y lo esperado es tener una merma progresiva en lo que a calidad perceptible se refiere (MOS / Mean Opinion Score).
Eso sí, estamos hablando del modelo básico, cercano a los 70€ en precio 😉 Si optamos por el PRO seguramente no tengamos ningún problema de este estilo.
Pero si nos mantenemos por debajo de ese umbral, lo esperado es tener una calidad perceptible cercana a la máxima que da el codec (3,92 en el caso de G729A, que es el que hemos probado).
Despedida y cierre
La verdad es que nos ha quedado un artículo similar al de los mecanismos de supervivencia que comentamos hace unos meses, pero queremos insistir humildemente en lo interesante que es la contratación de servicios de ITSP locales en lugar de optar por un revendedor mayor (aceptando los contras de perder gestión de trunks centralizada y depender de un ITSP que igual no es tan estable en el tiempo 😉 ).
En nuestra opinión, cuando se habla de proyectos Internacionales sin posibilidad de líneas físicas locales, y localizados en países como China, Tailandia o Colombia, este mecanismo nos está dando más que buenos resultados. De hecho, es un caso que hemos comentado recientemente en nuestra web, así que no dudéis en echarle un vistazo.
1 Comentario
¿Por qué no comentas tú también?
Mola Zgor. Bien explicado todo.
manwe Hace 8 años
Queremos tu opinión :)