Biblioteca de configuraciones de enrutamiento SBC
Encuentre el problema de enrutamiento que necesita resolver en ProSBC, copie la configuración en Ruby que lo resuelve y adáptela a su red. Puede probar cada una gratis en ProSBC Lab antes de que se acerque a llamadas reales.
Cómo funcionan estas configuraciones
La biblioteca cubre doce decisiones que los operadores automatizan en el borde de la sesión: enrutamiento de menor costo, failover entre operadores, controles contra el fraude telefónico, límites de tasa, normalización de encabezados y números, atestación STIR/SHAKEN por llamada y consultas a API externas. Cada configuración plantea el problema, el enfoque y los puntos de atención, ya que los modos de falla son lo que necesita saber antes de implementar.
Aquí el enrutamiento se escribe como código y no como campos de formulario, porque un formulario de configuración solo puede expresar lo que su creador pensó en prever, lo cual funciona hasta el día en que su enrutamiento depende de algo que no se previó. ProSBC expone el enrutamiento en Ruby, así que una decisión puede considerar cualquier dato disponible al establecer la llamada: la hora actual, la cantidad de llamadas en curso en cada operador, la respuesta de un servicio externo o si el número de origen es uno de los que usted asignó.
Dos de ellas son scripts de enrutamiento completos que heredan de BaseRouting. Las otras diez son módulos de filtro, que es como se empaquetan los propios módulos de enrutamiento de ProSBC: usted importa el archivo en Routing Scripts y luego agrega un require, un include y una línea de filtro a su script principal, normalmente simple_routing.rb. El encabezado de cada archivo indica esas líneas y cada columna de NAP o de ruta que lee. Todas siguen base_routing 1.42b (behavior version 2), así que revise su versión si usa una versión anterior.
Las configuraciones
Puede filtrar la lista según lo que intenta resolver. Haga clic en Copiar en cualquier configuración para obtener el archivo completo, o use su enlace para enviar a un colega directamente a ella.
12 configuraciones en 6 categorías.
Enrutamiento de menor costo (LCR) con tarifas por franja horaria
- Problema
- Usted compra terminación a varios operadores cuyas tarifas cambian según la hora, y quiere que cada llamada tome la ruta más barata disponible en ese momento.
- Enfoque
- Guarde las tarifas de cada operador en columnas de ruta personalizadas y luego proporcione un método de ordenamiento que clasifique las rutas coincidentes según la tarifa de la hora en curso. La coincidencia de rutas y la reasignación (remapping) siguen siendo las estándar.
- Puntos de atención
- La hora se revisa en cada llamada, así que el paso de tarifas pico a tarifas fuera de pico ocurre en la primera llamada posterior al cambio de franja. Las tarifas en sí son columnas de ruta que se leen al activar la configuración, de modo que cambiar una tarifa implica editar la ruta y volver a activar la configuración. Una ruta sin columnas de costo queda al final del orden, lo que la convierte en un respaldo natural: conserve una, para que un operador al que le falten tarifas no pueda dejar una llamada sin salida.
require 'base_routing'
# Least-cost routing with time-of-day rates. A complete routing script.
#
# Each route needs three custom route columns:
# cost_offpeak : rate applied 00:00-06:59
# cost_peak : rate applied 07:00-18:59
# cost : rate for all other hours, and the fallback
# Hours are the SBC's local time.
class LeastCostRouting < BaseRouting
route_match :call_field_name => :called
route_match :call_field_name => :nap
route_remap :call_field_name => :called, :route_field_name => :remapped_called
route_remap :call_field_name => :nap, :route_field_name => :remapped_nap
route_order :method => :order_by_current_cost
# Called for every call with the matching routes. Returns them cheapest first.
def order_by_current_cost(routes, nap_list)
column = rate_column_for(Time.now.hour)
routes.sort_by { |route| cost_of(route, column) }
end
private
def rate_column_for(hour)
case hour
when 0..6 then :cost_offpeak
when 7..18 then :cost_peak
else :cost
end
end
# A route with no usable cost sorts last instead of sorting as zero,
# which would make it look like the cheapest route.
def cost_of(route, column)
raw = route[column] || route[:cost]
raw.nil? ? Float::INFINITY : raw.to_f
end
end
@@routing = LeastCostRouting.new
def init_routes(routes)
@@routing.init routes
end
def route(call, nap_list)
@@routing.route call, nap_list
end
Conmutación por error (failover) entre operadores ante 503 y tiempo de espera agotado
- Problema
- Cuando un operador devuelve 503 Service Unavailable o deja de responder, las llamadas fallan en lugar de intentar con el siguiente operador.
- Enfoque
- Devuelva varias rutas en orden de prioridad y deje que el reintento de ruta (route retry) de ProSBC las recorra. Qué respuestas hacen pasar una llamada a la siguiente ruta se define por causa en el Reason Cause Mapping del perfil y no en el script, así que el script ordena las rutas candidatas y limita cuántas puede intentar una llamada.
- Puntos de atención
- Elija con criterio qué causas continúan. 503 y 408 se pueden reintentar sin riesgo, mientras que 404, 486 y la clase 6xx son respuestas reales sobre el destino, por lo que reintentarlas con otro operador desperdicia intentos y puede hacer que se llegue dos veces al mismo destino. Revise 603 en particular, porque el perfil predeterminado lo configura como Continue call. Un operador que nunca responde se detecta con el tiempo de espera del reintento de ruta, que puede configurar de forma global, por NAP o por ruta. Si no está seguro de lo que significa un código, búsquelo en el decodificador de códigos de respuesta SIP.
require 'base_routing'
# Priority routing with a capped route-retry list. A complete routing script.
#
# Which SIP responses move a call on to the next route is not decided in the
# script. It is the "Route retry action" of each cause in the profile's
# Reason Cause Mapping (Profiles > Edit Reason Cause Mapping):
# Continue call : 408, 500, 502, 503, 504 (this carrier could not take it)
# Stop call : 404, 484, 486, 6xx (a real answer about the destination)
# A carrier that never answers is caught by the route retry timeout
# (route_retry_mode and route_retry_timeout, set globally, per NAP or per route).
#
# Route column 'priority' (integer): 0 is tried first. An empty priority also
# counts as 0, so set it on every route.
class FailoverRouting < BaseRouting
MAX_ATTEMPTS = 3
route_match :call_field_name => :called
route_match :call_field_name => :nap
route_remap :call_field_name => :called, :route_field_name => :remapped_called
route_remap :call_field_name => :nap, :route_field_name => :remapped_nap
route_order :route_field_name => :priority
after_filter :method => :cap_attempts
# ProSBC tries the returned routes in order. Returning only the first
# MAX_ATTEMPTS stops a bad prefix from walking the whole carrier list.
def cap_attempts(params)
params[:routes] = params[:routes].first(MAX_ATTEMPTS)
params
end
end
@@routing = FailoverRouting.new
def init_routes(routes)
@@routing.init routes
end
def route(call, nap_list)
@@routing.route call, nap_list
end
Omitir los operadores que alcanzaron su límite de sesiones
- Problema
- Se ofrecen llamadas a un operador que ya no tiene capacidad, lo que genera respuestas 503 que se podían haber evitado.
- Enfoque
- Después de la coincidencia de rutas, descarte toda ruta cuyo NAP de destino esté en su límite contratado o cerca de él, con base en el conteo en vivo de llamadas salientes del NAP, para que nunca se ofrezca una llamada a un operador saturado.
- Puntos de atención
- Deje un margen en lugar de llenar exactamente hasta el límite contratado, porque un operador que cuenta las sesiones de forma ligeramente distinta rechazará llamadas que usted cree dentro de su cupo. Los NAP de ProSBC también pueden imponer por sí solos un máximo estricto de llamadas simultáneas; este filtro sirve para hacerse a un lado antes de llegar a ese punto, con el margen que usted elija. Para una visión más amplia, consulte estas estrategias de conmutación por error (failover) para VoIP.
# Skip destination carriers that are at, or close to, their session limit.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'capacity_headroom'
# include CapacityHeadroom # inside the routing class
# after_filter :method => :skip_full_carriers
#
# NAP column (Create New NAP Column):
# max_outgoing_call : integer, the carrier's contracted session limit.
# Leave it empty on NAPs that have no limit.
module CapacityHeadroom
HEADROOM = 0.95 # treat 95% of the limit as full
# Called once, when the configuration is activated.
def init_skip_full_carriers(params)
log_trace :always, "Using CapacityHeadroom (full at #{(HEADROOM * 100).round}% of max_outgoing_call)"
end
# Called for every call, after matching. Keeps only routes whose
# destination NAP still has room, using the NAP's live call count.
def skip_full_carriers(params)
naps = params[:naps]
params[:routes] = params[:routes].select do |route|
nap = naps[route[:remapped_nap].to_s.to_sym]
limit = nap ? nap[:max_outgoing_call].to_i : 0
limit <= 0 || nap[:inst_outgoing_call_cnt].to_i < limit * HEADROOM
end
raise RoutingException, :no_circuit_available if params[:routes].empty?
params
end
end
Bloquear números de origen según una lista que se mantiene al día
- Problema
- Necesita detener las llamadas de números de origen o prefijos específicos: una orden de bloqueo obligatoria, un patrón de fraude o una fuente de abuso conocida.
- Enfoque
- Cargue la lista una sola vez, al activar la configuración, y luego rechace las llamadas que coincidan antes de cualquier trabajo de enrutamiento. Las entradas coinciden como prefijos y también como números exactos, así que una sola entrada puede bloquear un rango completo.
- Puntos de atención
- Cargue la lista al activar, no en cada llamada. Leer un archivo en cada INVITE no resiste tasas de llamadas reales, y con este enfoque actualizar la lista significa importar el archivo nuevo y activar la configuración. Registre cada bloqueo junto con la entrada que coincidió, para poder demostrar por qué se rechazó una llamada. El motivo del rechazo se convierte en una respuesta SIP a través del Reason Cause Mapping del perfil, así que revise a qué se asigna
call_rejectedahí, y mantenga los bloqueos por política separados de las fallas de autorización para que sus CDR sigan siendo legibles. Para las órdenes de bloqueo que nombran a proveedores, consulte el seguimiento regulatorio de robocalls.
# Reject calls whose calling number matches a blocklist entry.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'calling_blocklist'
# include CallingBlocklist # inside the routing class
# before_filter :method => :reject_blocked_calling, :blocklist_file => 'blocklist.csv'
#
# blocklist.csv is imported as a custom file in the File DB. First line is the
# header; each entry is a full number or a prefix, with or without '+':
# prefix,reason
# 15551230000,blocking order 2026-014
# 1555999,fraud pattern
module CallingBlocklist
# Called once, when the configuration is activated, so no file is read on
# the call path. DbFile.get_file raises if the file has not been imported.
def init_reject_blocked_calling(params)
@blocked = {}
file = DbFile.get_file(params[:blocklist_file] || 'blocklist.csv')
file.csv_parse do |col_names, col_vals, row_idx|
prefix = col_vals['prefix'].to_s.strip.sub(/\A\+/, '')
@blocked[prefix] = col_vals['reason'].to_s unless prefix.empty?
end
log_trace :always, "CallingBlocklist loaded #{@blocked.size} entries"
end
# Called for every call, before any routing work. Checks each leading part
# of the number against the table, so the cost does not grow with the list.
def reject_blocked_calling(params)
calling = params[:call][:calling].to_s.sub(/\A\+/, '')
hit = (1..calling.length).map { |n| calling[0, n] }.find { |prefix| @blocked.key?(prefix) }
if hit
log_trace 1, "Blocked call from #{calling}: matched #{hit} (#{@blocked[hit]})"
raise RoutingException, :call_rejected
end
params
end
end
Restringir destinos costosos por horario y por troncal
- Problema
- El fraude telefónico suele presentarse como llamadas internacionales a destinos de alto costo, fuera del horario laboral, desde una extensión o una PBX comprometida.
- Enfoque
- Asigne a cada troncal una columna de NAP con los códigos de país a los que puede llamar, y limite los destinos de alto riesgo al horario laboral de los días hábiles. La verificación se ejecuta antes del enrutamiento, así que un endpoint comprometido no puede generar costos.
- Puntos de atención
- Este es el control antifraude más valioso que puede agregar, porque limita la exposición sin importar cómo se haya comprometido la credencial. Deniegue por defecto el tráfico internacional e incluya en la lista de permitidos los destinos a los que cada cliente realmente llama. El código de país 1 también llega a los países caribeños del NANP, que facturan con tarifas internacionales, por eso la lista de ejemplo nombra de forma explícita códigos de área como 876 y 809. La mayor parte del fraude telefónico ocurre de noche y los fines de semana, justo cuando nadie está vigilando. Más información sobre la prevención del fraude telefónico en tiempo real.
# Per-trunk destination allow-list, tightened outside business hours.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'destination_policy'
# include DestinationPolicy # inside the routing class
# before_filter :method => :enforce_destination_policy
#
# NAP column (Create New NAP Column), read on the incoming NAP:
# allowed_cc : country codes the trunk may call, space separated ('1 44 33'),
# or 'any'. Empty means '1' (domestic only, for a NANP operator).
#
# Called numbers are expected in E.164, with or without '+', at this point.
module DestinationPolicy
# Example list, replace it with your own. Includes Caribbean NANP area codes,
# which country code 1 also reaches. Allowed only Monday to Friday,
# 08:00-18:59, SBC local time.
HIGH_RISK = %w[1876 1809 212 224 225 234 235 252 372 373 508 509 675 676 677 678 679].freeze
BUSINESS_DAYS = (1..5) # Time#wday: 0 is Sunday
BUSINESS_HOURS = (8..18)
def init_enforce_destination_policy(params)
log_trace :always, "Using DestinationPolicy (#{HIGH_RISK.size} high-risk prefixes)"
end
def enforce_destination_policy(params)
call = params[:call]
nap = params[:naps][call[:nap].to_s.to_sym] || {}
called = call[:called].to_s.sub(/\A\+/, '')
allowed = nap[:allowed_cc].to_s.split
allowed = ['1'] if allowed.empty?
unless allowed.include?('any') || allowed.any? { |cc| called.start_with?(cc) }
log_trace 1, "Denied #{called} on #{call[:nap]}: destination not permitted"
raise RoutingException, :call_rejected
end
now = Time.now
in_hours = BUSINESS_DAYS.cover?(now.wday) && BUSINESS_HOURS.cover?(now.hour)
if HIGH_RISK.any? { |cc| called.start_with?(cc) } && !in_hours
log_trace 1, "Denied #{called} on #{call[:nap]}: high-risk destination out of hours"
raise RoutingException, :call_rejected
end
params
end
end
Limitar los intentos de llamada por número de origen
- Problema
- Un número de origen empieza a generar llamadas más rápido de lo que cualquier persona podría marcar, por un marcador en bucle o una extensión secuestrada, mientras el resto de su troncal se comporta con normalidad. Un límite por troncal no lo detecta, o bien frena a todos los demás clientes de esa troncal.
- Enfoque
- Mantenga una ventana deslizante de un segundo con los intentos de cada número de origen y rechace los que superen el límite, de modo que se frene a una sola fuente sin afectar a sus vecinos.
- Puntos de atención
- Para un límite simple por troncal, use la limitación de tasa de llamadas integrada en el NAP (un máximo de llamadas por segundo más una tolerancia de ráfaga) en lugar de un script, y reserve este para los límites que la configuración del NAP no puede expresar. La ventana vive en la memoria del script, así que empieza vacía cada vez que se activa la configuración, y los números inactivos se depuran para que la tabla no crezca sin límite. Las inundaciones de tráfico que no provienen de un solo número requieren protección contra denegación de servicio (DoS) SIP en el SBC.
# Sliding-window limit on call attempts per calling number.
#
# For a plain per-trunk limit, use the NAP's own call rate limiting
# (maximum calls per second and maximum burst) instead of a script.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'calling_rate_limit'
# include CallingRateLimit # inside the routing class
# before_filter :method => :limit_calling_rate
module CallingRateLimit
MAX_ATTEMPTS = 5 # attempts allowed per calling number...
WINDOW = 1.0 # ...in this many seconds
PRUNE_EVERY = 1000 # calls between clean-ups of idle numbers
# Called when the configuration is activated, so the window starts empty.
def init_limit_calling_rate(params)
@attempts = {}
@calls = 0
log_trace :always, "Using CallingRateLimit (#{MAX_ATTEMPTS} per #{WINDOW}s per calling number)"
end
def limit_calling_rate(params)
calling = params[:call][:calling].to_s
return params if calling.empty?
now = Time.now.to_f
recent = (@attempts[calling] ||= [])
recent.reject! { |t| now - t > WINDOW }
if recent.size >= MAX_ATTEMPTS
log_trace 1, "Rate limit hit for #{calling}: #{recent.size} attempts in #{WINDOW}s"
raise RoutingException, :no_circuit_available
end
recent << now
@calls += 1
prune_idle(now) if @calls % PRUNE_EVERY == 0
params
end
private
# Forget numbers with no attempt inside the window, so the table stays small.
def prune_idle(now)
@attempts.delete_if { |number, times| times.empty? || now - times.last > WINDOW }
end
end
Normalizar P-Asserted-Identity por operador
- Problema
- Un operador quiere la identidad de quien llama en P-Asserted-Identity, otro solo lee From y un tercero rechaza la llamada en cuanto aparece un PAI.
- Enfoque
- Lea una política desde una columna de cada NAP de destino: construya un PAI a partir del número de origen donde el operador lo exige, elimínelo donde el operador lo rechaza y déjelo pasar en todos los demás casos.
- Puntos de atención
- Resuelva esto antes de diagnosticar problemas de atestación. El servicio de firma toma la identidad de quien llama del PAI cuando hay uno, así que un PAI mal formado se manifiesta como lo que parece una falla de atestación. ProSBC presenta el PAI al script como los campos
private_addressy no como un encabezado sin procesar, y por eso esta configuración edita esos campos. Más información sobre la manipulación de encabezados SIP.
# Per-carrier P-Asserted-Identity policy.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'pai_policy'
# include PaiPolicy # inside the routing class
# after_remap_filter :method => :apply_pai_policy
#
# NAP columns (Create New NAP Column), read on the destination NAP:
# pai_policy : build|strip|pass. Empty means pass.
# build : assert the calling number in P-Asserted-Identity
# strip : send no P-Asserted-Identity or P-Preferred-Identity
# pass : forward whatever arrived
# pai_domain : optional host for the built identity, e.g. carrier.example.net
module PaiPolicy
def init_apply_pai_policy(params)
log_trace :always, "Using PaiPolicy"
end
# Called once per call, after remapping. out_calls[i] is the outgoing call
# that will be made on routes[i], so each carrier gets its own policy.
def apply_pai_policy(params)
params[:routes].each_with_index do |route, i|
out_call = params[:out_calls][i]
nap = params[:naps][route[:remapped_nap].to_s.to_sym] || {}
case nap[:pai_policy].to_s.downcase
when 'build'
calling = out_call[:calling].to_s
next if calling.empty?
out_call[:private_address] = calling
out_call[:private_address_sip_host] = nap[:pai_domain] unless nap[:pai_domain].to_s.empty?
log_trace 2, "PAI built for #{route[:remapped_nap]}: #{calling}"
when 'strip'
out_call[:private_address] = ''
out_call[:preferred_id] = ''
log_trace 2, "PAI stripped for #{route[:remapped_nap]}"
end
end
params
end
end
Agregar user=phone para los operadores que lo exigen
- Problema
- Un operador rechaza o enruta mal las llamadas si los URI SIP no llevan user=phone, mientras que sus demás operadores ni lo necesitan ni lo quieren.
- Enfoque
- Después de la reasignación (remapping), agregue user=phone a los parámetros de URI de From, To y P-Asserted-Identity en las llamadas a los operadores marcados en una columna del NAP, y conserve todos los parámetros que ya existan.
- Puntos de atención
- ProSBC entrega al script los parámetros de cada encabezado como tres cadenas (parámetros de usuario, de URI y de encabezado), así que esta configuración agrega el valor a los parámetros de URI en lugar de reconstruir el encabezado. Los encabezados que procesa la propia pila SIP, como Require, Supported y Privacy, nunca aparecen en el campo de encabezados personalizados del script, por lo que un script de enrutamiento no puede reescribirlos como encabezados sin procesar. La referencia de parámetros de script enumera los campos que un script puede modificar.
# Add user=phone to the From, To and P-Asserted-Identity URIs, per carrier.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'user_phone'
# include UserPhone # inside the routing class
# after_remap_filter :method => :add_user_phone
#
# NAP column (Create New NAP Column), read on the destination NAP:
# add_user_phone : boolean. Set it on the carriers that require user=phone.
module UserPhone
# SIP header parameter fields: From, To and P-Asserted-Identity.
FIELDS = [:calling_parameters, :called_parameters, :private_address_parameters].freeze
def init_add_user_phone(params)
log_trace :always, "Using UserPhone"
end
def add_user_phone(params)
params[:routes].each_with_index do |route, i|
nap = params[:naps][route[:remapped_nap].to_s.to_sym] || {}
next unless route_col_true?(nap[:add_user_phone])
out_call = params[:out_calls][i]
FIELDS.each do |field|
next if field == :private_address_parameters && out_call[:private_address].to_s.empty?
# Each field is a hash of :user_param, :uri_param and :header_param strings.
sip_params = out_call[field].is_a?(Hash) ? out_call[field] : {}
uri = sip_params[:uri_param].to_s
next if uri.split(';').include?('user=phone')
sip_params[:uri_param] = uri.empty? ? 'user=phone' : "#{uri};user=phone"
out_call[field] = sip_params
end
log_trace 2, "user=phone added for #{route[:remapped_nap]}"
end
params
end
end
Normalizar todos los números a E.164 en la salida
- Problema
- 404 Not Found o 484 Address Incomplete, porque un operador quiere +1XXXXXXXXXX, otro 1XXXXXXXXXX y un tercero solo los 10 dígitos.
- Enfoque
- Convierta internamente cada número de destino y de origen a su forma canónica E.164, y luego, en la salida, aplíquele el formato que indique la columna de cada NAP de destino.
- Puntos de atención
- Normalice al entrar y aplique el formato al salir. Una única representación interna canónica es lo que mantiene esto manejable a medida que se agregan troncales, mientras que traducir directamente entre los formatos de cada operador produce una matriz de reglas que crece de forma cuadrática. Todo lo que no sea un número, como anonymous, se deja intacto. Más información sobre el formato de número E.164.
# Canonicalize numbers to E.164, then format them the way each carrier expects.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'e164_format'
# include E164Format # inside the routing class
# after_remap_filter :method => :format_numbers
#
# NAP column (Create New NAP Column), read on the destination NAP:
# number_format : plus|cc|nsn. Empty means plus.
# plus -> +15551234567
# cc -> 15551234567
# nsn -> 5551234567 (national significant number)
module E164Format
DEFAULT_CC = '1'
def init_format_numbers(params)
log_trace :always, "Using E164Format (default country code #{DEFAULT_CC})"
end
# Runs after the route's own remapping, once per outgoing call.
def format_numbers(params)
params[:routes].each_with_index do |route, i|
out_call = params[:out_calls][i]
nap = params[:naps][route[:remapped_nap].to_s.to_sym] || {}
style = nap[:number_format].to_s.downcase
style = 'plus' if style.empty?
[:called, :calling].each do |field|
e164 = e164_canonical(out_call[field])
out_call[field] = e164_formatted(e164, style) unless e164.empty?
end
end
params
end
private
# Digits only, with the country code present. Anything that is not a
# number (anonymous, alphanumeric) comes back empty and is left alone.
def e164_canonical(number)
digits = number.to_s.gsub(/[^0-9+]/, '')
return '' if digits.delete('+').empty?
if digits.start_with?('+')
digits[1..-1]
elsif digits.start_with?('011') # NANP international prefix
digits[3..-1]
elsif digits.length == 10 # bare NANP national number
DEFAULT_CC + digits
else
digits
end
end
def e164_formatted(e164, style)
case style
when 'plus' then "+#{e164}"
when 'nsn' then e164.start_with?(DEFAULT_CC) ? e164[DEFAULT_CC.length..-1] : e164
else e164
end
end
end
Elegir la atestación A, B o C por llamada
- Problema
- Un solo SBC transporta tráfico minorista, mayorista y de gateway. Una configuración de atestación única por troncal es una declaración falsa en algunas llamadas o una degradación innecesaria en otras.
- Enfoque
- Decida el nivel a partir de la clase de tráfico de la troncal de entrada y de si el número de origen es uno que usted asignó a ese cliente; luego envíe la decisión solo en el INVITE que va al servicio de firma, y elimine cualquier nivel que un cliente haya intentado enviar.
- Puntos de atención
- Otorgar atestación de nivel A a un número que usted no puede verificar es una declaración falsa según las reglas de la FCC, y otorgar C a sus propios suscriptores minoristas perjudica la completación de sus llamadas. La decisión tiene que ser suya y no de su proveedor de firma, y FCC 24-120 lo establece de forma explícita. La forma en que el nivel llega al servicio de firma depende de cada proveedor, así que el nombre del encabezado en el archivo es un marcador de posición que debe reemplazar por el que documente su STI-AS. En producción, ProSBC se comunica con sus socios de firma, TransNexus ClearIP y Neustar, por SIP, y por eso esta configuración se basa en el NAP cuyo
service_typees AUTHENTICATION.
# Decide attestation A, B or C per call, and pass the decision to the signing
# service on the INVITE that goes to it.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'attestation_policy'
# include AttestationPolicy # inside the routing class
# after_remap_filter :method => :select_attestation
#
# NAP columns (Create New NAP Column):
# traffic_class : retail|wholesale|gateway on each incoming NAP. Empty means gateway.
# service_type : NORMAL|AUTHENTICATION|VERIFICATION, as in the ClearIP setup.
# The level is sent only to NAPs marked AUTHENTICATION.
#
# issued_numbers.csv is imported as a custom file in the File DB:
# prefix,nap
# 1555200,retail-a
# 1555300,retail-b
module AttestationPolicy
# PLACEHOLDER. Use the header name and values your signing service documents
# for the requested attestation level.
ATTEST_HEADER = 'X-Attestation-Level'
def init_select_attestation(params)
@issued = {}
file = DbFile.get_file(params[:issued_file] || 'issued_numbers.csv')
file.csv_parse do |col_names, col_vals, row_idx|
prefix = col_vals['prefix'].to_s.strip.sub(/\A\+/, '')
(@issued[col_vals['nap'].to_s] ||= []) << prefix unless prefix.empty?
end
log_trace :always, "AttestationPolicy loaded number ranges for #{@issued.size} NAPs"
end
def select_attestation(params)
call = params[:call]
ingress = params[:naps][call[:nap].to_s.to_sym] || {}
calling = call[:calling].to_s.sub(/\A\+/, '')
level =
case ingress[:traffic_class].to_s.downcase
when 'retail' then issued_to?(call[:nap].to_s, calling) ? 'A' : 'B'
when 'wholesale' then 'B'
else 'C'
end
params[:routes].each_with_index do |route, i|
out_call = params[:out_calls][i]
dest = params[:naps][route[:remapped_nap].to_s.to_sym] || {}
# Never forward a level that arrived from the customer.
headers = out_call[:sip_header].to_s.split("\n").reject { |h| attest_header?(h) }
headers << "#{ATTEST_HEADER}: #{level}" if dest[:service_type].to_s.upcase == 'AUTHENTICATION'
out_call[:sip_header] = headers.empty? ? '' : headers.join("\n") + "\n"
end
log_trace 2, "Attestation #{level} for #{calling} from #{call[:nap]}"
params
end
private
def issued_to?(nap_name, calling)
(@issued[nap_name] || []).any? { |prefix| calling.start_with?(prefix) }
end
def attest_header?(line)
line.split(':', 2).first.to_s.strip.casecmp(ATTEST_HEADER) == 0
end
end
Enrutar según el resultado de verificación (verstat)
- Problema
- Las llamadas entrantes llegan con un resultado de verificación y usted quiere tratar de forma distinta el tráfico verificado, el fallido y el no firmado, en lugar de ignorarlo.
- Enfoque
- Lea verstat de los parámetros de URI de P-Asserted-Identity o de From; luego envíe las llamadas fallidas a las rutas marcadas para ellas, como un anuncio o una cola de revisión, y deje que todo lo demás se enrute con normalidad.
- Puntos de atención
- La distinción que importa es entre TN-Validation-Failed y No-TN-Validation. Failed significa que se revisó un token y no pasó, mientras que No-TN-Validation significa que no había nada que revisar, por lo general porque la llamada llegó sin firma, y tratar ambos casos por igual marcaría volúmenes enormes de tráfico legítimo. Con TransNexus ClearIP como servicio de verificación, verstat regresa en el P-Asserted-Identity de su respuesta 302. El decodificador de PASSporT explica qué significa cada valor de verstat.
# Route inbound calls on the STIR/SHAKEN verification result (verstat).
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'verstat_policy'
# include VerstatPolicy # inside the routing class
# after_filter :method => :branch_on_verstat
#
# Route column (Create New Route Column):
# failed_verification : boolean. Set it on the route(s) that should take calls
# whose verification failed, such as an announcement
# or a review queue. Other calls never use those routes.
module VerstatPolicy
def init_branch_on_verstat(params)
log_trace :always, "Using VerstatPolicy"
end
def branch_on_verstat(params)
call = params[:call]
verstat = read_verstat(call)
divert = params[:routes].select { |route| route_col_true?(route[:failed_verification]) }
normal = params[:routes] - divert
if verstat == 'TN-Validation-Failed' && !divert.empty?
# A token was present and did not validate. Divert rather than drop.
log_trace 1, "Verification failed for #{call[:calling]}: diverting"
params[:routes] = divert
else
# Passed, No-TN-Validation or no verstat at all. Unsigned is not
# suspicious on its own, so normal policy decides.
log_trace 2, "verstat #{verstat || 'absent'} for #{call[:calling]}"
params[:routes] = normal
end
params
end
private
# verstat is a parameter of the P-Asserted-Identity or From URI. Each
# parameter field is a hash of :user_param, :uri_param and :header_param.
def read_verstat(call)
[:private_address_parameters, :calling_parameters].each do |field|
value = call[field]
text = value.is_a?(Hash) ? value.values.join(';') : value.to_s
text.split(';').each do |pair|
name, setting = pair.split('=', 2)
return setting.to_s.strip if name.to_s.strip.casecmp('verstat') == 0
end
end
nil
end
end
Consultar una API externa y enrutar según la respuesta
- Problema
- El enrutamiento depende de datos que el SBC no tiene: una consulta LNP (LNP dip), una puntuación de fraude, el plan actual de un cliente o una búsqueda en el CRM.
- Enfoque
- Use la consulta HTTP de ProSBC. El script describe la solicitud y lanza
http_query_required, ProSBC la ejecuta con un tiempo de espera estricto y el script se ejecuta de nuevo con la respuesta, que usa para reasignar el número de destino, rechazar la llamada o poner en primer lugar al operador que indicó el servicio. - Puntos de atención
- Todo el diseño gira en torno a la política ante fallas. El script nunca espera a la red por sí mismo, pero la llamada sí, así que el tiempo de espera tiene que caber dentro de su margen de retardo posmarcación (post-dial delay). Decida de antemano qué significa una consulta lenta o fallida: falla abierta (fail open) para enriquecer el enrutamiento y falla cerrada (fail closed) para bloquear el fraude. Deje esa elección explícita en las opciones del filtro.
# Ask an external service how to route a call, using ProSBC's HTTP query.
# The script does not wait on the network itself: it describes the query,
# ProSBC runs it, and the routing script runs again with the answer.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'external_route_lookup'
# include ExternalRouteLookup # inside the routing class
# before_filter :method => :external_route_lookup,
# :lookup_url => 'https://routing.example.com/v1/lookup',
# :timeout_ms => 1500,
# :fail_open => true
# after_filter :method => :prefer_lookup_nap
#
# Expected JSON answer: {"called": "...", "nap": "...", "block": false, "reason": "..."}
require 'uri'
require 'json'
module ExternalRouteLookup
def init_external_route_lookup(params)
@lookup_url = params[:lookup_url].to_s
@timeout_ms = (params[:timeout_ms] || 1500).to_i # keep well inside your post-dial delay budget
@fail_open = params.key?(:fail_open) ? params[:fail_open] : true
log_trace :always, "Using ExternalRouteLookup: #{@lookup_url} (#{@timeout_ms} ms, fail #{@fail_open ? 'open' : 'closed'})"
end
def external_route_lookup(params)
call = params[:call]
if params[:http_query].nil?
# First pass: describe the query and ask ProSBC to run it.
query = URI.encode_www_form(:called => call[:called].to_s, :calling => call[:calling].to_s)
params[:http_query] = {
:url => "#{@lookup_url}?#{query}",
:use_post => false,
:timeout_ms => @timeout_ms,
:headers_hash => { 'Accept' => 'application/json' }
}
raise RoutingException, :http_query_required
end
# Second pass: the result is in params[:http_query].
answer = parse_answer(params[:http_query])
if answer.nil?
log_trace 1, "Routing lookup failed: #{@fail_open ? 'routing normally' : 'rejecting'}"
raise RoutingException, :temporary_failure unless @fail_open
return params
end
if answer['block']
log_trace 1, "Routing lookup blocked #{call[:called]}: #{answer['reason']}"
raise RoutingException, :call_rejected
end
call[:called] = answer['called'].to_s if answer['called']
if answer['nap']
params[:user_context] ||= {}
params[:user_context][:lookup_nap] = answer['nap'].to_s
end
params
end
# After matching: try the NAP the service named first, keep the rest as backup.
def prefer_lookup_nap(params)
wanted = (params[:user_context] || {})[:lookup_nap]
return params if wanted.nil?
first = params[:routes].select { |route| route[:remapped_nap].to_s.casecmp(wanted) == 0 }
params[:routes] = first + (params[:routes] - first)
params
end
private
# nil unless the service answered 200 with a JSON object.
def parse_answer(http_query)
return nil unless http_query[:response_value].to_i == 200
answer = JSON.parse(http_query[:response_data].to_s)
answer.is_a?(Hash) ? answer : nil
rescue JSON::ParserError
nil
end
end
Recomendamos validar cada configuración antes de implementarla, ya que son puntos de partida para adaptar a su red y no código listo para producción. Ese paso importa más en el enrutamiento que en la mayoría del código, porque un script de enrutamiento se ejecuta en la ruta de la llamada: si encuentra un error que no maneja, ProSBC rechaza la llamada como una falla temporal. Analizamos cada archivo con Prism, el analizador sintáctico oficial de Ruby, y ejecutamos cada uno en un entorno de pruebas basado en la interfaz BaseRouting documentada. Eso detecta errores de escritura y de lógica, pero no puede indicarle cómo se comporta una configuración con su tráfico, que es para lo que sirve una prueba en ProSBC Lab o en una instancia de prueba.
¿Tiene un problema de enrutamiento que resolver?
Cuéntenos qué debe hacer su enrutamiento y podemos conversar sobre cómo ProSBC puede adaptarse a sus necesidades. Mientras tanto, ProSBC Lab le ofrece una instancia gratuita y permanente de tres sesiones para crear y probar. Para una visión más amplia, lea nuestra guía sobre el enrutamiento de llamadas basado en API.
Al enviar este formulario, su información será procesada de acuerdo con nuestra Política de privacidad.
¿Prefiere ponerlo en práctica primero? Cree un laboratorio ProSBC gratuito y pruébelo usted mismo.