Biblioteca de configurações de roteamento SBC

Encontre o problema de roteamento que você precisa resolver no ProSBC, copie a configuração em Ruby que o resolve e adapte-a à sua rede. Você pode testar cada uma gratuitamente no ProSBC Lab antes que ela chegue perto de chamadas reais.

Como estas configurações funcionam

A biblioteca cobre doze decisões que as operadoras automatizam na borda da sessão: roteamento de menor custo, failover entre operadoras, controles contra fraude telefônica, limites de taxa, normalização de cabeçalhos e números, atestação STIR/SHAKEN por chamada e consultas a APIs externas. Cada configuração apresenta o problema, a abordagem e os pontos de atenção, já que os modos de falha são o que você precisa conhecer antes de implantar.

Aqui, o roteamento é escrito como código, e não como campos de formulário, porque um formulário de configuração só consegue expressar o que quem o criou pensou em prever, o que funciona até o dia em que o seu roteamento depender de algo que não foi previsto. O ProSBC expõe o roteamento em Ruby, então uma decisão pode considerar qualquer coisa disponível no estabelecimento da chamada: a hora atual, o número de chamadas em andamento em cada operadora, a resposta de um serviço externo ou se o número chamador é um dos que você emitiu.

Duas delas são scripts de roteamento completos que herdam de BaseRouting. As outras dez são módulos de filtro, que é como os próprios módulos de roteamento do ProSBC são empacotados: você importa o arquivo em Routing Scripts e depois adiciona um require, um include e uma linha de filtro ao seu script principal, normalmente o simple_routing.rb. O cabeçalho de cada arquivo lista essas linhas e cada coluna de NAP ou de rota que ele lê. Todas seguem o base_routing 1.42b (behavior version 2), então verifique a sua versão se você usa uma versão mais antiga.

As configurações

Você pode filtrar a lista pelo que está tentando resolver. Clique em Copiar em qualquer configuração para pegar o arquivo inteiro, ou use o link dela para mandar um colega direto para ela.

Todas (12)Roteamento de chamadas (3)Segurança e fraude (3)Cabeçalhos SIP (2)Formatos de número (1)STIR/SHAKEN (2)Consultas via API (1)

12 configurações em 6 categorias.

Roteamento de menor custo (LCR) com tarifas por horário

Roteamento de chamadasScript completo

Problema
Você compra terminação de várias operadoras cujas tarifas mudam conforme a hora e quer que cada chamada siga a rota mais barata disponível naquele momento.
Abordagem
Armazene as tarifas de cada operadora em colunas personalizadas de rota e forneça um método de ordenação que classifique as rotas correspondentes pela tarifa da hora atual. A correspondência e o remapeamento seguem o comportamento padrão.
Pontos de atenção
A hora é verificada a cada chamada, então a troca da tarifa de pico para a tarifa fora de pico acontece na primeira chamada depois da virada do horário. As tarifas em si são colunas de rota, lidas quando a configuração é ativada, então alterar uma tarifa significa editar a rota e ativar de novo. Uma rota sem colunas de custo fica por último na ordenação, o que faz dela uma rota de contingência natural: mantenha uma, para que uma operadora com tarifas faltando não deixe nenhuma chamada sem saída.
Ruby · least_cost_routing.rbCopiarLink
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

Failover entre operadoras em respostas 503 e timeouts

Roteamento de chamadasScript completo

Problema
Quando uma operadora retorna 503 Service Unavailable ou para de responder, as chamadas falham em vez de tentar a próxima operadora.
Abordagem
Retorne várias rotas em ordem de prioridade e deixe o mecanismo de nova tentativa de rota (route retry) do ProSBC percorrer a lista. Quais respostas fazem a chamada avançar para a próxima rota é definido por causa no Reason Cause Mapping do perfil, e não no script, então o script ordena as candidatas e limita quantas uma chamada pode tentar.
Pontos de atenção
Defina com cuidado quais causas continuam a chamada. 503 e 408 podem ser repetidos com segurança, enquanto 404, 486 e a classe 6xx são respostas reais sobre o destino, então tentar de novo em outra operadora desperdiça tentativas e pode alcançar o mesmo destino duas vezes. Verifique o 603 em especial, porque o perfil padrão o define como Continue call. Uma operadora que nunca responde é detectada pelo timeout de nova tentativa de rota, que você pode definir globalmente, por NAP ou por rota. Se você não tiver certeza do que um código significa, consulte o decodificador de códigos de resposta SIP.
Ruby · failover_routing.rbCopiarLink
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

Pular operadoras que atingiram o limite de sessões

Roteamento de chamadasMódulo de filtro

Problema
As chamadas são oferecidas a uma operadora sem capacidade disponível, gerando respostas 503 que poderiam ter sido evitadas.
Abordagem
Depois da correspondência, descarte toda rota cujo NAP de destino esteja no limite contratado ou perto dele, usando a contagem em tempo real de chamadas de saída do NAP, para que uma operadora saturada nunca receba uma chamada.
Pontos de atenção
Deixe uma folga em vez de preencher exatamente até o limite contratado, porque uma operadora que conta sessões de um jeito um pouco diferente vai rejeitar chamadas que você acredita estarem dentro da cota. Os NAPs do ProSBC também podem impor por conta própria um máximo rígido de chamadas simultâneas, e este filtro serve para desviar as chamadas antes de chegar a esse ponto, com a margem que você escolher. Para uma visão mais ampla, veja estas estratégias de failover para VoIP.
Ruby · capacity_headroom.rbCopiarLink
# 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 chamadores com base em uma lista mantida por você

Segurança e fraudeMódulo de filtro

Problema
Você precisa barrar chamadas de números chamadores ou prefixos específicos: uma ordem de bloqueio obrigatória, um padrão de fraude ou uma origem abusiva conhecida.
Abordagem
Carregue a lista uma única vez, quando a configuração for ativada, e rejeite as chamadas correspondentes antes de qualquer trabalho de roteamento. As entradas valem tanto como prefixos quanto como números exatos, então uma única entrada pode bloquear uma faixa inteira.
Pontos de atenção
Carregue na ativação, não a cada chamada. Ler um arquivo a cada INVITE não aguenta taxas reais de chamadas e, nesse modelo, atualizar a lista significa importar o novo arquivo e ativar a configuração. Registre em log cada bloqueio com a entrada correspondente, para poder mostrar por que uma chamada foi rejeitada. O motivo da rejeição vira uma resposta SIP por meio do Reason Cause Mapping do perfil, então verifique para o que call_rejected está mapeado ali, e mantenha os bloqueios por política separados das falhas de autorização para que seus CDRs continuem legíveis. Para ordens de bloqueio que citam provedores, consulte o monitor regulatório de robocalls.
Ruby · calling_blocklist.rbCopiarLink
# 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 caros por horário e por tronco

Segurança e fraudeMódulo de filtro

Problema
A fraude telefônica costuma aparecer como chamadas internacionais para destinos de alto custo, fora do horário comercial, a partir de um ramal ou PBX comprometido.
Abordagem
Dê a cada tronco uma coluna de NAP com os códigos de país que ele pode alcançar e restrinja os destinos de alto risco ao horário comercial dos dias úteis. A verificação ocorre antes do roteamento, então um terminal comprometido não consegue gerar custo.
Pontos de atenção
Este é o controle antifraude de maior valor que você pode adicionar, porque limita a exposição independentemente de como a credencial foi comprometida. Negue as chamadas internacionais por padrão e coloque na lista de permissão os destinos que cada cliente realmente chama. O código de país 1 também alcança os países caribenhos do NANP, que são cobrados com tarifas internacionais, por isso a lista de exemplo cita explicitamente códigos de área como 876 e 809. A maior parte da fraude telefônica acontece de madrugada e nos fins de semana, justamente quando ninguém está de olho. Saiba mais sobre prevenção de fraude telefônica em tempo real.
Ruby · destination_policy.rbCopiarLink
# 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 tentativas de chamada por número chamador

Segurança e fraudeMódulo de filtro

Problema
Um número chamador começa a fazer chamadas mais rápido do que qualquer pessoa conseguiria discar, por causa de um discador em loop ou de um ramal invadido, enquanto o restante do tronco segue normal. Um limite por tronco ou não percebe o problema ou desacelera todos os outros clientes desse tronco.
Abordagem
Mantenha uma janela deslizante de um segundo com as tentativas de cada número chamador e rejeite as que passarem do limite, para frear uma única origem sem afetar as vizinhas.
Pontos de atenção
Para um limite simples por tronco, use a limitação de taxa de chamadas nativa do NAP (um máximo de chamadas por segundo mais uma tolerância de rajada) em vez de um script, e reserve este para limites que a configuração do NAP não consegue expressar. A janela fica na memória do script, então começa vazia sempre que a configuração é ativada, e os números ociosos são descartados para que a tabela não cresça sem limite. Ataques de inundação que não partem de um único número exigem proteção contra DoS em SIP no SBC.
Ruby · calling_rate_limit.rbCopiarLink
# 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 o P-Asserted-Identity por operadora

Cabeçalhos SIPMódulo de filtro

Problema
Uma operadora quer a identidade de origem no P-Asserted-Identity, outra só lê o From, e uma terceira rejeita a chamada pelo simples fato de haver um PAI.
Abordagem
Leia uma política de uma coluna em cada NAP de destino: monte um PAI a partir do número chamador quando a operadora exigir, remova-o quando a operadora não aceitar e repasse-o sem alteração nos demais casos.
Pontos de atenção
Acerte isso antes de investigar problemas de atestação. O serviço de assinatura obtém a identidade de origem do PAI quando ele está presente, então um PAI malformado se manifesta como algo que parece uma falha de atestação. O ProSBC apresenta o PAI ao script como os campos private_address, e não como um cabeçalho bruto, e é por isso que este script edita esses campos. Saiba mais sobre manipulação de cabeçalhos SIP.
Ruby · pai_policy.rbCopiarLink
# 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

Adicionar user=phone para as operadoras que exigem

Cabeçalhos SIPMódulo de filtro

Problema
Uma operadora rejeita ou roteia errado as chamadas se os URIs SIP não trouxerem user=phone, enquanto suas outras operadoras não precisam disso nem querem.
Abordagem
Depois do remapeamento, adicione user=phone aos parâmetros de URI de From, To e P-Asserted-Identity nas chamadas para as operadoras marcadas em uma coluna de NAP, mantendo todos os parâmetros que já estiverem presentes.
Pontos de atenção
O ProSBC entrega ao script os parâmetros de cada cabeçalho como três strings (parâmetros de usuário, de URI e de cabeçalho), então este script acrescenta aos parâmetros de URI em vez de reconstruir o cabeçalho. Cabeçalhos que a própria pilha SIP processa, como Require, Supported e Privacy, nunca aparecem no campo de cabeçalhos personalizados do script, então um script de roteamento não consegue reescrevê-los como cabeçalhos brutos. A referência de parâmetros de script lista os campos que um script pode alterar.
Ruby · user_phone.rbCopiarLink
# 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 os números para E.164 na saída

Formatos de númeroMódulo de filtro

Problema
404 Not Found ou 484 Address Incomplete, porque uma operadora quer +1XXXXXXXXXX, outra 1XXXXXXXXXX e uma terceira só os 10 dígitos.
Abordagem
Converta internamente cada número chamado e chamador para a forma canônica E.164 e, na saída, formate-o como indicar a coluna de cada NAP de destino.
Pontos de atenção
Normalize na entrada, formate na saída. Uma única representação interna canônica é o que torna a solução fácil de manter à medida que novos troncos entram, enquanto traduzir diretamente entre os formatos das operadoras gera uma matriz de regras que cresce de forma quadrática. Tudo o que não for número, como anonymous, fica intocado. Saiba mais sobre o formato de número E.164.
Ruby · e164_format.rbCopiarLink
# 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

Escolher a atestação A, B ou C por chamada

STIR/SHAKENMódulo de filtro

Problema
Um mesmo SBC transporta tráfego de varejo, de atacado e de gateway. Uma única configuração de atestação por tronco acaba sendo uma declaração falsa em algumas chamadas ou um rebaixamento desnecessário em outras.
Abordagem
Defina o nível com base na classe de tráfego do tronco de entrada e no fato de o número chamador ter sido atribuído por você àquele cliente. Depois, envie a decisão apenas no INVITE destinado ao serviço de assinatura, removendo qualquer nível que um cliente tenha tentado enviar.
Pontos de atenção
Atestar no nível A um número que você não consegue verificar é uma declaração falsa pelas regras da FCC, e atestar C para seus próprios assinantes de varejo prejudica o completamento das chamadas deles. A decisão precisa ser sua, e não do seu fornecedor de assinatura, e a FCC 24-120 deixa isso explícito. A forma como o nível chega ao serviço de assinatura depende do provedor, então o nome de cabeçalho no arquivo é provisório e deve ser trocado pelo que o seu STI-AS documentar. Em produção, o ProSBC se conecta aos seus parceiros de assinatura, TransNexus ClearIP e Neustar, via SIP, e é por isso que este script se baseia no NAP cujo service_type é AUTHENTICATION.
Ruby · attestation_policy.rbCopiarLink
# 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

Rotear pelo resultado da verificação (verstat)

STIR/SHAKENMódulo de filtro

Problema
As chamadas de entrada chegam com um resultado de verificação, e você quer tratar de forma diferente o tráfego verificado, o que falhou e o não assinado, em vez de ignorar essa informação.
Abordagem
Leia o verstat dos parâmetros de URI do P-Asserted-Identity ou do From e envie as chamadas com falha para as rotas marcadas para elas, como uma mensagem gravada ou uma fila de revisão, deixando todo o resto seguir o roteamento normal.
Pontos de atenção
A distinção que importa é entre TN-Validation-Failed e No-TN-Validation. Failed significa que um token foi verificado e não passou, enquanto No-TN-Validation significa que não havia nada para verificar, geralmente porque a chamada chegou sem assinatura, e tratar os dois da mesma forma sinalizaria volumes enormes de tráfego legítimo. Com o TransNexus ClearIP como serviço de verificação, o verstat volta no P-Asserted-Identity da resposta 302 que ele envia. O decodificador de PASSporT explica o que significa cada valor de verstat.
Ruby · verstat_policy.rbCopiarLink
# 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 uma API externa e rotear pela resposta

Consultas via APIMódulo de filtro

Problema
O roteamento depende de dados que o SBC não tem: uma consulta de portabilidade numérica (LNP), uma pontuação de fraude, o plano atual de um cliente ou uma consulta ao CRM.
Abordagem
Use a consulta HTTP do ProSBC. O script descreve a requisição e lança http_query_required, o ProSBC a executa com um timeout rígido, e o script roda de novo com a resposta, que ele usa para remapear o número chamado, rejeitar a chamada ou colocar em primeiro lugar a operadora indicada pelo serviço.
Pontos de atenção
Todo o design gira em torno da política de falha. O script nunca espera pela rede, mas a chamada espera, então o timeout precisa caber no seu orçamento de atraso pós-discagem (post-dial delay). Decida com antecedência o que uma consulta lenta ou com falha significa: fail open (a chamada segue) para enriquecimento de roteamento, fail closed (a chamada é barrada) para bloqueio de fraude. Torne essa escolha explícita nas opções do filtro.
Ruby · external_route_lookup.rbCopiarLink
# 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 configuração antes de implantá-la, já que elas são pontos de partida para adaptar à sua rede, e não código pronto para produção. Essa etapa importa mais no roteamento do que na maioria dos códigos, porque um script de roteamento roda no caminho da chamada: se encontrar um erro que não trata, o ProSBC recusa a chamada como uma falha temporária. Analisamos cada arquivo com o Prism, o parser oficial do Ruby, e executamos cada um em um ambiente de testes construído sobre a interface BaseRouting documentada. Isso pega erros de digitação e de lógica, mas não diz como uma configuração se comporta com o seu tráfego, e é para isso que serve um teste no ProSBC Lab ou em uma instância de avaliação.

Tem um problema de roteamento para resolver?

Conte-nos o que o seu roteamento precisa fazer e podemos conversar sobre como o ProSBC pode atender às suas necessidades. Enquanto isso, o ProSBC Lab oferece uma instância gratuita e permanente de três sessões para você montar e testar. Para uma visão mais ampla, leia nosso guia sobre roteamento de chamadas orientado por API.

Ao enviar este formulário, suas informações serão processadas de acordo com nossa Política de Privacidade.