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.

Todas (12)Enrutamiento de llamadas (3)Seguridad y fraude (3)Encabezados SIP (2)Formatos de número (1)STIR/SHAKEN (2)Consultas a API (1)

12 configuraciones en 6 categorías.

Enrutamiento de menor costo (LCR) con tarifas por franja horaria

Enrutamiento de llamadasScript completo

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.
Ruby · least_cost_routing.rbCopiarEnlace
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

Enrutamiento de llamadasScript completo

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.
Ruby · failover_routing.rbCopiarEnlace
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

Enrutamiento de llamadasMódulo de filtro

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.
Ruby · capacity_headroom.rbCopiarEnlace
# 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

Seguridad y fraudeMódulo de filtro

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_rejected ahí, 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.
Ruby · calling_blocklist.rbCopiarEnlace
# 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

Seguridad y fraudeMódulo de filtro

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.
Ruby · destination_policy.rbCopiarEnlace
# 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

Seguridad y fraudeMódulo de filtro

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.
Ruby · calling_rate_limit.rbCopiarEnlace
# 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

Encabezados SIPMódulo de filtro

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_address y 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.
Ruby · pai_policy.rbCopiarEnlace
# 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

Encabezados SIPMódulo de filtro

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.
Ruby · user_phone.rbCopiarEnlace
# 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

Formatos de númeroMódulo de filtro

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.
Ruby · e164_format.rbCopiarEnlace
# 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

STIR/SHAKENMódulo de filtro

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_type es AUTHENTICATION.
Ruby · attestation_policy.rbCopiarEnlace
# 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)

STIR/SHAKENMódulo de filtro

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.
Ruby · verstat_policy.rbCopiarEnlace
# 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

Consultas a APIMódulo de filtro

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.
Ruby · external_route_lookup.rbCopiarEnlace
# 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.