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.
12 configurations réparties en 6 catégories.
Routage au moindre coût (LCR) avec tarifs horaires
- 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.
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
- 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.
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
- 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.
# 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
- 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_rejectedy 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.
# 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
- 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.
# 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
- 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.
# 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
- 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_addressplutô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.
# 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
- 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.
# 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
- 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.
# 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
- 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_typeest AUTHENTICATION.
# Decide attestation A, B or C per call, and pass the decision to the signing
# service on the INVITE that goes to it.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
# require 'attestation_policy'
# include AttestationPolicy # inside the routing class
# after_remap_filter :method => :select_attestation
#
# NAP columns (Create New NAP Column):
# traffic_class : retail|wholesale|gateway on each incoming NAP. Empty means gateway.
# service_type : NORMAL|AUTHENTICATION|VERIFICATION, as in the ClearIP setup.
# The level is sent only to NAPs marked AUTHENTICATION.
#
# issued_numbers.csv is imported as a custom file in the File DB:
# prefix,nap
# 1555200,retail-a
# 1555300,retail-b
module AttestationPolicy
# PLACEHOLDER. Use the header name and values your signing service documents
# for the requested attestation level.
ATTEST_HEADER = 'X-Attestation-Level'
def init_select_attestation(params)
@issued = {}
file = DbFile.get_file(params[:issued_file] || 'issued_numbers.csv')
file.csv_parse do |col_names, col_vals, row_idx|
prefix = col_vals['prefix'].to_s.strip.sub(/\A\+/, '')
(@issued[col_vals['nap'].to_s] ||= []) << prefix unless prefix.empty?
end
log_trace :always, "AttestationPolicy loaded number ranges for #{@issued.size} NAPs"
end
def select_attestation(params)
call = params[:call]
ingress = params[:naps][call[:nap].to_s.to_sym] || {}
calling = call[:calling].to_s.sub(/\A\+/, '')
level =
case ingress[:traffic_class].to_s.downcase
when 'retail' then issued_to?(call[:nap].to_s, calling) ? 'A' : 'B'
when 'wholesale' then 'B'
else 'C'
end
params[:routes].each_with_index do |route, i|
out_call = params[:out_calls][i]
dest = params[:naps][route[:remapped_nap].to_s.to_sym] || {}
# Never forward a level that arrived from the customer.
headers = out_call[:sip_header].to_s.split("\n").reject { |h| attest_header?(h) }
headers << "#{ATTEST_HEADER}: #{level}" if dest[:service_type].to_s.upcase == 'AUTHENTICATION'
out_call[:sip_header] = headers.empty? ? '' : headers.join("\n") + "\n"
end
log_trace 2, "Attestation #{level} for #{calling} from #{call[:nap]}"
params
end
private
def issued_to?(nap_name, calling)
(@issued[nap_name] || []).any? { |prefix| calling.start_with?(prefix) }
end
def attest_header?(line)
line.split(':', 2).first.to_s.strip.casecmp(ATTEST_HEADER) == 0
end
end
Router les appels selon le résultat de vérification (verstat)
- 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.
# 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
- 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.
# 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é.
Vous préférez d’abord passer à la pratique ? Créez un lab ProSBC gratuit et testez-le vous-même.