Bibliothèque de configurations de routage SBC

Trouvez le problème de routage que vous devez résoudre sur ProSBC, copiez la configuration Ruby qui le résout et adaptez-la à votre réseau. Vous pouvez tester chacune gratuitement sur ProSBC Lab avant qu’elle n’approche le moindre appel réel.

Comment fonctionnent ces configurations

La bibliothèque couvre douze décisions que les opérateurs automatisent en bordure de session : routage au moindre coût, basculement entre opérateurs, contrôles contre la fraude à la taxation, limites de débit d’appels, normalisation des en-têtes et des numéros, attestation STIR/SHAKEN appel par appel et requêtes vers des API externes. Chaque configuration expose le problème, l’approche et les points de vigilance, car ce sont les modes de défaillance qu’il faut connaître avant de déployer.

Ici, le routage s’écrit en code plutôt qu’en champs de formulaire, car un formulaire de configuration ne peut exprimer que ce que son concepteur a pensé à prévoir, ce qui convient jusqu’au jour où votre routage dépend d’un cas qu’il n’a pas envisagé. ProSBC expose plutôt le routage en Ruby : une décision peut donc s’appuyer sur tout ce qui est disponible à l’établissement de l’appel, qu’il s’agisse de l’heure courante, du nombre d’appels en cours chez chaque opérateur, de la réponse d’un service externe ou du fait que le numéro appelant fait partie de ceux que vous avez attribués.

Deux d’entre elles sont des scripts de routage complets qui héritent de BaseRouting. Les dix autres sont des modules de filtre, le format dans lequel sont livrés les propres modules de routage de ProSBC : vous importez le fichier sous Routing Scripts, puis vous ajoutez un require, un include et une ligne de filtre à votre script principal, généralement simple_routing.rb. L’en-tête de chaque fichier indique ces lignes ainsi que chaque colonne de NAP ou de route qu’il lit. Toutes suivent base_routing 1.42b (behavior version 2) : vérifiez votre version si vous utilisez une version plus ancienne.

Les configurations

Vous pouvez filtrer la liste selon ce que vous cherchez à corriger. Cliquez sur Copier pour récupérer le fichier complet d’une configuration, ou utilisez son lien pour y envoyer directement un collègue.

Toutes (12)Routage des appels (3)Sécurité et fraude (3)En-têtes SIP (2)Formats de numéros (1)STIR/SHAKEN (2)Requêtes API (1)

12 configurations réparties en 6 catégories.

Routage au moindre coût (LCR) avec tarifs horaires

Routage des appelsScript complet

Problème
Vous achetez de la terminaison auprès de plusieurs opérateurs dont les tarifs varient selon l’heure, et vous voulez que chaque appel emprunte la route la moins chère disponible à cet instant.
Approche
Stockez les tarifs de chaque opérateur dans des colonnes de route personnalisées, puis fournissez une méthode de tri qui classe les routes correspondantes selon le tarif de l’heure en cours. La mise en correspondance et le remappage restent standard.
Points de vigilance
L’heure est vérifiée à chaque appel, si bien que le passage des tarifs de pointe aux tarifs creux intervient dès le premier appel qui suit le changement de plage horaire. Les tarifs eux-mêmes sont des colonnes de route, lues à l’activation de la configuration : pour modifier un tarif, il faut donc modifier la route puis activer à nouveau la configuration. Une route sans colonnes de coût est classée en dernier, ce qui en fait une solution de repli naturelle. Gardez-en une, pour qu’un opérateur dont les tarifs manquent ne puisse jamais laisser un appel sans issue.
Ruby · least_cost_routing.rbCopierLien
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

Basculement entre opérateurs sur 503 et sur expiration du délai

Routage des appelsScript complet

Problème
Lorsqu’un opérateur renvoie 503 Service Unavailable ou cesse de répondre, les appels échouent au lieu d’essayer l’opérateur suivant.
Approche
Renvoyez plusieurs routes par ordre de priorité et laissez la nouvelle tentative de routage de ProSBC (route retry) les parcourir. Les réponses qui font passer un appel à la route suivante se définissent cause par cause dans le Reason Cause Mapping du profil, et non dans le script. Le script ordonne donc les routes candidates et limite le nombre de routes qu’un appel peut essayer.
Points de vigilance
Choisissez délibérément les causes qui poursuivent l’appel. Les codes 503 et 408 peuvent être réessayés sans risque, alors que 404, 486 et la classe 6xx sont de vraies réponses sur la destination : les réessayer sur un autre opérateur gaspille des tentatives et peut joindre deux fois la même destination. Vérifiez 603 en particulier, car le profil par défaut le règle sur « Continue call ». Un opérateur qui ne répond jamais est intercepté par le délai de nouvelle tentative de route (route retry timeout), que vous pouvez définir globalement, par NAP ou par route. En cas de doute sur la signification d’un code, consultez le décodeur de codes de réponse SIP.
Ruby · failover_routing.rbCopierLien
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

Écarter les opérateurs qui ont atteint leur limite de sessions

Routage des appelsModule de filtre

Problème
Des appels sont proposés à un opérateur qui n’a plus de capacité disponible, ce qui produit des 503 qui auraient pu être évités.
Approche
Après la mise en correspondance, retirez chaque route dont le NAP de destination a atteint ou approche sa limite contractuelle, d’après le nombre d’appels sortants en temps réel de ce NAP, pour qu’un opérateur saturé ne se voie jamais proposer d’appel.
Points de vigilance
Gardez une marge plutôt que de remplir exactement jusqu’à la limite contractuelle, car un opérateur qui compte les sessions un peu différemment rejettera des appels que vous croyez dans les limites. Les NAP de ProSBC peuvent aussi imposer d’eux-mêmes un nombre maximal strict d’appels simultanés, et ce filtre sert à s’écarter avant ce seuil, avec la marge de votre choix. Pour une vue d’ensemble, consultez ces stratégies de basculement VoIP.
Ruby · capacity_headroom.rbCopierLien
# 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

Bloquer des numéros appelants à partir d’une liste tenue à jour

Sécurité et fraudeModule de filtre

Problème
Vous devez stopper les appels provenant de numéros appelants ou de préfixes précis : une ordonnance de blocage obligatoire, un schéma de fraude ou une source abusive connue.
Approche
Chargez la liste une seule fois, à l’activation de la configuration, puis rejetez les appels correspondants avant tout traitement de routage. Les entrées s’appliquent comme préfixes autant que comme numéros exacts, de sorte qu’une seule entrée peut bloquer toute une plage.
Points de vigilance
Chargez la liste à l’activation, pas à chaque appel. Une lecture de fichier à chaque INVITE ne tiendra pas face à des cadences d’appels réelles, et mettre la liste à jour revient alors à importer le nouveau fichier puis à activer la configuration. Journalisez chaque blocage avec l’entrée qui a correspondu, pour pouvoir justifier le rejet d’un appel. Le motif de rejet devient une réponse SIP via le Reason Cause Mapping du profil : vérifiez donc à quoi call_rejected y est associé, et séparez les blocages liés à une politique des échecs d’autorisation pour que vos CDR restent lisibles. Pour les ordonnances de blocage qui désignent des fournisseurs, consultez le suivi réglementaire des appels automatisés.
Ruby · calling_blocklist.rbCopierLien
# 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

Restreindre les destinations coûteuses selon l’horaire et la jonction

Sécurité et fraudeModule de filtre

Problème
La fraude à la taxation se manifeste généralement par des appels internationaux vers des destinations à coût élevé, en dehors des heures de bureau, depuis un poste ou un PBX compromis.
Approche
Attribuez à chaque jonction une colonne de NAP qui liste les indicatifs de pays qu’elle peut joindre, et limitez les destinations à risque élevé aux heures de bureau en semaine. La vérification a lieu avant le routage, de sorte qu’un terminal compromis ne peut engendrer aucun coût.
Points de vigilance
C’est, à lui seul, le contrôle antifraude le plus rentable que vous puissiez ajouter, car il plafonne l’exposition quelle que soit la manière dont les identifiants ont été compromis. Interdisez l’international par défaut et placez sur liste d’autorisation les destinations que chaque client appelle réellement. L’indicatif de pays 1 dessert aussi les pays caribéens du NANP, facturés aux tarifs internationaux, et c’est pourquoi la liste d’exemple nomme explicitement des indicatifs régionaux comme 876 et 809. La plupart des fraudes à la taxation surviennent la nuit et le week-end, précisément quand personne ne surveille. En savoir plus sur la prévention de la fraude à la taxation en temps réel.
Ruby · destination_policy.rbCopierLien
# 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

Limiter les tentatives d’appel par numéro appelant

Sécurité et fraudeModule de filtre

Problème
Un numéro appelant se met à passer des appels plus vite qu’un humain ne pourrait composer, à cause d’un numéroteur qui tourne en boucle ou d’un poste piraté, alors que le reste de sa jonction se comporte normalement. Un plafond par jonction passe à côté ou ralentit tous les autres clients de cette jonction.
Approche
Tenez, pour chaque numéro appelant, une fenêtre glissante d’une seconde des tentatives, et rejetez celles qui dépassent la limite, de sorte qu’une source soit ralentie sans affecter ses voisines.
Points de vigilance
Pour une simple limite par jonction, utilisez la limitation de cadence d’appels intégrée au NAP (un nombre maximal d’appels par seconde plus une tolérance de rafale) plutôt qu’un script, et réservez celui-ci aux limites que le paramètre du NAP ne permet pas d’exprimer. La fenêtre réside dans la mémoire du script : elle repart donc à vide à chaque activation de la configuration, et les numéros inactifs sont purgés pour que la table ne puisse pas grossir indéfiniment. Les inondations d’appels qui ne proviennent pas d’un seul numéro nécessitent une protection contre le déni de service (DoS) SIP au niveau du SBC.
Ruby · calling_rate_limit.rbCopierLien
# 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

Normaliser l’en-tête P-Asserted-Identity par opérateur

En-têtes SIPModule de filtre

Problème
Un opérateur veut l’identité de l’appelant dans P-Asserted-Identity, un autre ne lit que From, et un troisième rejette l’appel dès qu’un PAI est présent.
Approche
Lisez une politique dans une colonne de chaque NAP de destination : construisez un PAI à partir du numéro appelant là où l’opérateur en exige un, supprimez-le là où l’opérateur s’y oppose, et transmettez-le tel quel partout ailleurs.
Points de vigilance
Réglez ce point avant de dépanner des problèmes d’attestation. Le service de signature prend l’identité de l’appelant dans le PAI lorsqu’il y en a un, si bien qu’un PAI mal formé se manifeste par ce qui ressemble à un défaut d’attestation. ProSBC présente le PAI au script sous la forme des champs private_address plutôt que comme un en-tête brut, c’est pourquoi ce script modifie ces champs. En savoir plus sur la manipulation des en-têtes SIP.
Ruby · pai_policy.rbCopierLien
# 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

Ajouter user=phone pour les opérateurs qui l’exigent

En-têtes SIPModule de filtre

Problème
Un opérateur rejette ou achemine mal les appels si les URI SIP ne portent pas user=phone, alors que vos autres opérateurs n’en ont pas besoin et n’en veulent pas.
Approche
Après le remappage, ajoutez user=phone aux paramètres d’URI de From, To et P-Asserted-Identity sur les appels destinés aux opérateurs marqués dans une colonne de NAP, en conservant tous les paramètres déjà présents.
Points de vigilance
ProSBC transmet au script les paramètres de chaque en-tête sous forme de trois chaînes (paramètres utilisateur, d’URI et d’en-tête) : ce script complète donc les paramètres d’URI au lieu de reconstruire l’en-tête. Les en-têtes que la pile SIP traite elle-même, comme Require, Supported et Privacy, n’apparaissent jamais dans le champ d’en-têtes personnalisés du script, si bien qu’un script de routage ne peut pas les réécrire comme en-têtes bruts. La référence des paramètres de script liste les champs qu’un script peut modifier.
Ruby · user_phone.rbCopierLien
# 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

Normaliser chaque numéro au format E.164 en sortie

Formats de numérosModule de filtre

Problème
Des erreurs 404 Not Found ou 484 Address Incomplete, parce qu’un opérateur veut +1XXXXXXXXXX, un autre 1XXXXXXXXXX et un troisième les 10 chiffres seuls.
Approche
Convertissez en interne chaque numéro appelé et appelant au format canonique E.164, puis mettez-le en forme en sortie comme l’indique la colonne de chaque NAP de destination.
Points de vigilance
Normaliser en entrée, formater en sortie. C’est une représentation interne canonique unique qui garde l’ensemble maintenable à mesure que des jonctions s’ajoutent, alors que traduire directement d’un format d’opérateur à l’autre produit une matrice de règles qui croît de façon quadratique. Tout ce qui n’est pas un numéro, comme anonymous, reste intact. En savoir plus sur le format de numéro E.164.
Ruby · e164_format.rbCopierLien
# 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

Choisir l’attestation A, B ou C pour chaque appel

STIR/SHAKENModule de filtre

Problème
Un même SBC achemine du trafic de détail, du trafic de gros et du trafic de passerelle. Un réglage d’attestation unique par jonction constitue soit une fausse déclaration sur certains appels, soit une rétrogradation inutile sur d’autres.
Approche
Déterminez le niveau à partir de la classe de trafic de la jonction d’entrée et du fait que le numéro appelant est, ou non, un numéro que vous avez attribué à ce client, puis envoyez la décision uniquement sur l’INVITE destiné au service de signature, en supprimant tout niveau qu’un client aurait tenté d’envoyer.
Points de vigilance
Attester au niveau A un numéro que vous ne pouvez pas vérifier constitue une fausse déclaration au regard des règles de la FCC, et attribuer le niveau C à vos propres abonnés de détail nuit à l’aboutissement de leurs appels. Cette décision doit vous revenir, et non à votre fournisseur de signature, ce que l’ordonnance FCC 24-120 rend explicite. La façon dont le niveau parvient au service de signature dépend du fournisseur : le nom d’en-tête qui figure dans le fichier est donc un espace réservé, à remplacer par celui que documente votre STI-AS. En production, ProSBC joint ses partenaires de signature, TransNexus ClearIP et Neustar, via SIP, c’est pourquoi ce script s’appuie sur le NAP dont le service_type est AUTHENTICATION.
Ruby · attestation_policy.rbCopierLien
# 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

Router les appels selon le résultat de vérification (verstat)

STIR/SHAKENModule de filtre

Problème
Les appels entrants arrivent avec un résultat de vérification, et vous voulez traiter différemment le trafic vérifié, le trafic en échec et le trafic non signé plutôt que d’ignorer ce résultat.
Approche
Lisez verstat dans les paramètres d’URI de P-Asserted-Identity ou de From, puis envoyez les appels en échec vers les routes marquées pour eux, comme une annonce ou une file d’examen, et laissez tout le reste suivre le routage normal.
Points de vigilance
La distinction qui compte est celle entre TN-Validation-Failed et No-TN-Validation. Failed signifie qu’un jeton a été vérifié et n’a pas passé le contrôle, alors que No-TN-Validation signifie qu’il n’y avait rien à vérifier, généralement parce que l’appel est arrivé non signé. Traiter les deux de la même façon signalerait d’énormes volumes de trafic légitime. Avec TransNexus ClearIP comme service de vérification, verstat revient dans l’en-tête P-Asserted-Identity de sa réponse 302. Le décodeur PASSporT détaille la signification de chaque valeur de verstat.
Ruby · verstat_policy.rbCopierLien
# 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

Interroger une API externe et router selon la réponse

Requêtes APIModule de filtre

Problème
Le routage dépend de données que le SBC ne détient pas : une interrogation LNP (LNP dip), un score de fraude, l’offre actuelle d’un client ou une recherche dans un CRM.
Approche
Utilisez la requête HTTP de ProSBC. Le script décrit la requête et lève http_query_required, ProSBC l’exécute avec un délai d’expiration strict, puis le script s’exécute à nouveau avec la réponse, qu’il utilise pour remapper le numéro appelé, rejeter l’appel ou placer en premier l’opérateur désigné par le service.
Points de vigilance
Toute la conception repose sur la politique d’échec. Le script n’attend jamais lui-même le réseau, mais l’appel, lui, attend : le délai d’expiration doit donc tenir dans votre budget de délai post-numérotation. Décidez à l’avance de ce que signifie une consultation lente ou en échec : laisser passer (fail open) pour l’enrichissement du routage, refuser (fail closed) pour le blocage antifraude, et rendez ce choix explicite dans les options du filtre.
Ruby · external_route_lookup.rbCopierLien
# 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

Nous recommandons de valider chaque configuration avant de la déployer, car il s’agit de points de départ à adapter à votre réseau, et non de code prêt pour la production. Cette étape compte davantage pour le routage que pour la plupart des codes, car un script de routage s’exécute sur le chemin de l’appel : s’il rencontre une erreur qu’il ne gère pas, ProSBC refuse l’appel comme un échec temporaire. Nous avons analysé chaque fichier avec Prism, l’analyseur syntaxique officiel de Ruby, et exécuté chacun sur un banc de test construit à partir de l’interface BaseRouting documentée. Cela détecte les fautes de frappe et les erreurs de logique, mais ne peut pas vous dire comment une configuration se comporte avec votre trafic : c’est le rôle d’un essai sur ProSBC Lab ou sur une instance d’évaluation.

Un problème de routage à résoudre ?

Dites-nous ce que votre routage doit faire, et nous pourrons voir ensemble comment ProSBC peut répondre à vos besoins. En attendant, ProSBC Lab vous donne une instance gratuite et permanente de trois sessions pour construire et tester. Pour une vue d’ensemble, lisez notre guide sur le routage des appels piloté par API.

En soumettant ce formulaire, vos informations seront traitées conformément à notre Politique de confidentialité.