Passa al contenuto principale

Akira — Routing Model v5.16

Data: 2026-05-13 (sessione 2 pomeriggio) Riferimento docx: cap. 18 (Routing Plans), cap. 20 (Surcharge Filters), cap. 13 (Destinations) Schema DB: akira_schema_v1.sql — tabelle routing_plans, routing_rules, routing_rule_attempts, routing_attempt_trunks, destination_groups, access_policies, blocked_calls Mockup riferimento: akira-mockup.html route #/routing/plans, #/routing/plans/:id, #/routing/plans/:id/configure/:destName, #/routing/surcharge-filters


0. Scopo

Questo documento consolida il modello concettuale Routing di Akira come deciso nella sessione del 13 maggio 2026 dopo il feedback critico dell'utente. Allinea il design a quanto Stealth (legacy) faceva storicamente — pattern "Routing by Destination" — chiarendo:

  • Cosa è un Routing Plan e perché contiene SEMPRE tutte le destinations
  • Come funziona il Routing by Destination editor (3+1 colonne attempt × N trunk weighted)
  • Cosa sono i Surcharge Filters (ex "Access Policies") e perché di default è pass-through
  • Il flow runtime completo Kamailio per ogni INVITE

Le decisioni qui sono vincolanti per:

  • Implementazione backend services/routing/ (Fase 3a)
  • Implementazione frontend pagine route routing/plans e routing/surcharge-filters (Fase 3a)
  • Configurazione Kamailio dispatcher + htable + acc_json (Fase 2)

1. Le tre entità del modello

1.1 Destinations (cap. 13)

  • Catalogo globale di destinazioni telco wholesale: "Italy Mobile TIM", "UK Mobile (all)", "Premium Africa", ...
  • Identificate da name univoco + country_iso2 (derivato da E.164 cap. 56) + lista di prefixes versionati con valid_from/valid_to.
  • Una destination NON ha riferimenti diretti a routing, tariff o terminator — è una pura categoria di traffico.
  • Catalogo MOCK attuale: 10 destinations (vedi MOCK.destinations nel mockup).

1.2 Routing Plan (cap. 18)

  • Un Routing Plan è una mappa logica "destination → attempt-list" che vive per un perimetro di Originator (uno o più Originator possono puntare allo stesso plan via originator.routing_plan_id).
  • Ogni plan ha 3 attributi globali:
    • technique ∈ {LCR, Percent, Round Robin} (cap. 18.6)
    • failover_policy di default (rerouteable causes Q.850 + SIP, PDD threshold, invite_timeout, max_total_attempts cap. 18.7 v5.16 = 4)
    • circuit_breaker config (cap. 18.8 — failure threshold, window, open duration, half-open probe)
  • Un plan contiene SEMPRE tutte le destinations del catalogo, anche se vuote.
  • Coverage % = count(destinations configurate) / count(catalogo). Plan "incompleto" = coverage < 100% (le restanti destinations cadono in no-routing).

1.3 Surcharge Filters (cap. 20, ex "Access Policies")

  • Regole pre-routing che applicano block / allow_only / surcharge a chiamate verso destination groups specifici.
  • Default behavior: pass-through. Se nessun filter matcha → la chiamata passa al Routing Plan senza modifiche.
  • I filter sono eccezioni, non gate obbligatori. Una lista vuota = tutto passa.
  • Tabelle DB: destination_groups (tag-based o manual member-list) + access_policies (la regola) + access_policy_authorized_originators (whitelist per allow_only).
  • Audit trail in blocked_calls (hypertable TimescaleDB 1d chunk, 90gg retention).

2. Il pattern "Destination → Attempt-list"

Per ogni (plan, destination) configurato, esiste una lista ordinata di attempt (slot 1..4 cap. 18.7). Ogni attempt contiene N trunk weighted (terminator + weight %).

2.1 Struttura attempt

SlotNome UICosa fa runtimeTrunk per slot
1First attemptPrimo tentativo. Distribuisce il traffico su N trunk weighted secondo technique.1..N (default: 1)
2Second attemptFailover #1 se First attempt fallisce con SIP/Q.850 rerouteable cause.1..N
3Third attemptFailover #2.1..N
4Fourth attempt (final fallback)Ultimo tentativo. Nessun ulteriore reroute (cap. 18.7 final). Tipicamente trunk premium-cost.1..N

Esempio Italy Mobile Vodafone (plan italy-mobile-lcr-2026q2):

First attempt:
60% sparkle_italy_mobile · supplier rate €0.0280
30% bics_global_premium · €0.0310
10% tata_asia · €0.0340
Second attempt (failover):
100% apex_failover (final) · €0.0380
Third attempt: (vuoto)
Fourth attempt: (vuoto)

2.2 Comportamento per technique

  • LCR (Least Cost Routing): l'algoritmo ignora i weight % e ordina i trunk dello slot per supplier_rate crescente. Tenta il più economico, poi il successivo, ecc.
  • Percent (% weighted): distribuzione probabilistica secondo il weight. Es. 60/30/10 → 60% del traffico va al primo trunk, 30% al secondo, 10% al terzo. La somma dei weight per slot DEVE essere 100 (validazione UI con "Update Percentages" button).
  • Round Robin: alternanza ciclica fra i trunk dello slot, ignorando i weight.

2.3 No-routing (→ SIP 503)

Se la destination matched non ha attempt configurati nel plan attivo dell'Originator, Kamailio ritorna immediatamente SIP 503 Service Unavailable con header Reason: cause=503; text="no routing".

Casi tipici di no-routing:

  • Plan appena creato (vuoto di default — TUTTE le destinations sono in no-routing finché non si configura).
  • Destination nuova aggiunta al catalogo dopo la creazione del plan (non automaticamente popolata).
  • Errore di configurazione (admin ha eliminato gli attempt per quella destination).

Decisione di design: NO fallback automatico. Una destination senza routing = 503 deterministico. Il monitoring deve catturare il 503 con reason "no routing" e alertare admin via Pattern Analyzer (cap. 46) o SLA Policy (cap. 27).


3. Routing by Destination — UI editor (cap. 18.6)

Pagina dedicata #/routing/plans/:planId/configure/:destName (pattern Stealth classico). Sostituisce il modal openEditAttempts perché 4 colonne side-by-side richiedono spazio full-page.

3.1 Layout

┌─────────────────────────────────────────────────────────────────────────┐
│ Breadcrumb: routing / plans / italy-mobile-lcr-2026q2 / configure │
├─────────────────────────────────────────────────────────────────────────┤
│ Title: Routing by Destination [📈 View Selling Prices] │
│ italy-mobile-lcr-2026q2 [← back] [💾 Save] │
├─────────────────────────────────────────────────────────────────────────┤
│ Destination dropdown: │
│ [ALL DESTINATIONS / Italy Mobile Vodafone / ... ] [badge stato] │
├─────────────────────────────────────────────────────────────────────────┤
│ Banner contestuale: configured / no-routing / bulk mode │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │ First │ Second │ Third │ Fourth │ │
│ │ attempt │ attempt │ attempt │ (final) │ │
│ │ │ │ │ │ │
│ │ Add Trunk │ Add Trunk │ Add Trunk │ Add Trunk │ │
│ │ [select] │ [select] │ [select] │ [select] │ │
│ │ [+ Add] │ [+ Add] │ [+ Add] │ [+ Add] │ │
│ │ │ │ │ │ │
│ │ Trunk|%|✕ │ Trunk|%|✕ │ Trunk|%|✕ │ Trunk|%|✕ │ │
│ │ 60% sparkle │ 100% apex │ (vuoto) │ (vuoto) │ │
│ │ 30% bics │ │ │ │ │
│ │ 10% tata │ │ │ │ │
│ │ Total: 100% │ Total: 100% │ │ │ │
│ │ │ │ │ │ │
│ │ [↻ Update] │ [↻ Update] │ ... │ ... │ │
│ │ [✕ Del All] │ [✕ Del All] │ │ │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ Failover policy: │
│ [conservative ▼] PDD: [6s] Timeout: [30s] Max attempts: [4] │
└─────────────────────────────────────────────────────────────────────────┘

3.2 Modalità ALL DESTINATIONS (bulk)

Quando dal dropdown è selezionato ALL DESTINATIONS:

  • Permette di configurare un routing default che viene applicato a tutte le destinations non ancora configurate.
  • Banner giallo "BULK MODE — applica default routing a tutte le destinations senza routing (oggi: N)".
  • Save crea N nuove rules (una per ogni destination no-routing) usando gli attempt configurati nelle 4 colonne.
  • Le destinations già configurate NON vengono toccate (override sicuro).

3.3 View Selling Prices (modal cross-check margine)

Bottone in alto destra (visibile solo se destination specifica selezionata). Apre modal con:

  • 3 KPI: Best supplier cost (rate del trunk più economico nel First attempt) / Avg selling rate (rate medio venduto ai customer per questa destination) / Margin medio (% e €/min).
  • Tabella per ogni customer × tariff che vende questa destination: Rate venduta · Increment · Cost supplier (best) · Margin €/min · Margin % · Valid from.
  • Sezione "Offer attive override" (cap. 14.5) se esistono offer puntuali.
  • Nota tecnica su billing_increment (60+60 inflaziona margin % per chiamate brevi).
  • Action: Export selling prices · Drill-down Reports.

Serve a verificare prima di toccare il routing che la modifica non eroda il margine sotto soglia accettabile.


4. Surcharge Filters (cap. 20) — semantica e UI

4.1 Modello concettuale

I Surcharge Filters sono regole pre-routing che modificano il comportamento standard pass-through. Senza filter, ogni chiamata che passa l'auth va dritto al Routing Plan.

4.2 Le 3 action

ActionEffetto runtimeSIP responseUse case tipico
blockRifiuta INVITE403 Forbidden / 603 DeclineAnti-Wangiri/IRSF su prefissi premium-rate; protezione retail customer da destinazioni rischiose.
allow_onlyPermetti SOLO ai matched originator (whitelist), blocca tutti gli altri verso quel destination group403 per non-matchedDestination satellitari/iridium permesse solo a Originator con contratto premium.
surchargeAggiungi +€/min al rate Tariff per chiamate matchednessuna (passa al routing con cost override)Surcharge +€0.20/min per retail su premium-rate (compensare il rischio fraud + margin).

4.3 Struttura dati

  • destination_groups: collezione di destinations. Due kind:
    • tag_based: membership automatica da destinations.tags (GIN index). Es. group "Premium-rate-blocked" = tag premium OR iridium OR sat.
    • manual: lista esplicita di destination ids.
  • access_policies (= un filter): riferimento a destination_group_id, action, surcharge (se action=surcharge), applies_to (originator tier o lista specifica).
  • access_policy_authorized_originators: tabella N:M per allow_only (chi è autorizzato).
  • blocked_calls: hypertable Timescale (1d chunk, 90gg retention) — audit per ogni chiamata bloccata. Permette pattern detection (Pattern Analyzer cap. 46).

4.4 UI key points

  • Default state: empty-state con "✓ Nessun filter configurato, tutto il traffico passa di default" + CTA "+ Crea primo filter". Niente alert verde di "tutto bene" che possa essere frainteso.
  • Banner cyan in cima con flow runtime esplicito: Auth → Surcharge Filter check → Routing Plan → INVITE outbound.
  • Tabella filter attivi mostra: nome, group, action (badge colorato), applies_to, surcharge €/min, blocked 24h.
  • Tabella destination_groups separata (un group può essere usato da N filter).
  • Audit blocked_calls in fondo con timestamp, originator, CLI masked, DNIS, filter applicato, SIP response.
  • Pattern alert (banner warning) se più di N block sullo stesso originator/destination in 24h — link a Pattern Analyzer.

5. Flow runtime Kamailio (per ogni INVITE)

┌────────────────────────────────────────────────────────────────┐
│ INVITE arriva su sip-01:5060 (UDP) │
└──────────────────────────────┬─────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ STEP 1 — Auth (cap. 5.4 + 11.5 + 12.5) │
│ • IP whitelist match? (Originator.inbound_ip_whitelist) │
│ • Digest auth? (HA1 lookup via auth_db, cap. 12.5 v5.16 │
│ HA1 cifrato AES-256-GCM in devices.sip_ha1_encrypted) │
│ • pike_check_req() anti-flood │
│ • PASS → step 2 · FAIL → 401/403 │
└──────────────────────────────┬─────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ STEP 2 — Destination matching (cap. 13) │
│ • longest_prefix_match(dst) → destination_id │
│ • Lookup via Kamailio htable (sincronizzata da DB 30s, │
│ cap. 18.12 + Redis pub/sub immediate per blocchi urgenti) │
│ • PASS → step 3 · FAIL → 404 (unknown destination) │
└──────────────────────────────┬─────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ STEP 3 — Surcharge Filter check (cap. 20) │
│ • Cerca destination_group dove dest appartiene + filter attivi │
│ • Per ogni filter matched, valuta action vs originator │
│ • SE action=block → 403 Forbidden + audit blocked_calls │
│ • SE action=allow_only + originator NON autorizzato → 403 │
│ • SE action=surcharge → applica +€/min nel cost_runtime │
│ • SE nessun filter → default pass-through, no-op │
│ • PASS → step 4 │
└──────────────────────────────┬─────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ STEP 4 — Routing Plan lookup (cap. 18) │
│ • plan = originator.routing_plan_id │
│ • attempts = plan.rules[destination_name] │
│ • SE attempts vuoto → 503 Service Unavailable │
│ + audit (no_routing reason) │
│ • SE attempts.length >= 1 → step 5 │
└──────────────────────────────┬─────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ STEP 5 — Attempt loop (max 4, cap. 18.7 v5.16) │
│ for slot in attempts: │
│ • Seleziona trunk dello slot via technique │
│ (LCR: ordina per rate / Percent: weighted / RR: ciclica) │
│ • Circuit breaker check (cap. 18.8): trunk open? skip │
│ • Forward INVITE outbound │
│ • Aspetta response │
│ • SE 200 OK → success, CDR insert · loop end │
│ • SE 18x → wait answer/timeout │
│ • SE 4xx/5xx + cause IN failover_policy.rerouteable → │
│ next slot │
│ • SE final cause (404, 603 BUSY, ecc.) → no reroute, │
│ final response al chiamante │
│ end-for │
│ · SE tutti slot esauriti → final response (cause dell'ultimo) │
└──────────────────────────────┬─────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│ STEP 6 — Post-call (cap. 25) │
│ • CDR insert in Timescale hypertable via NATS JetStream │
│ (ADR-0007, batch insert 10k/s sustained) │
│ • Rating runtime: customer rate × billing_increment + │
│ surcharge_eur_per_min (se applicato in step 3) │
│ • Update Redis hot-state (CB counters, active calls) │
│ • Audit log se action sensibile │
└────────────────────────────────────────────────────────────────┘

5.1 Performance budget (cap. 4.1 — zero-DB sul hot path)

  • Step 1 (auth): lookup in htable Kamailio. Target latenza < 5ms.
  • Step 2 (destination match): longest_prefix_match SQL puro fino ~50k prefix; oltre → Redis trie cache. Target < 10ms.
  • Step 3 (surcharge filter): htable lookup pre-computed (destination_group_id → list of filters). Target < 2ms.
  • Step 4 (routing lookup): htable lookup (plan_id, destination_id) → attempt-list. Target < 5ms.
  • Totale step 1-4 (pre-INVITE outbound): < 25ms. PDD additivo solo dello step 5 (waiting carrier response).

5.2 Tabelle Redis hot-state (sincronizzate da DB)

Chiave RedisContenutoTTLRefresh
dest:prefix:39destination_id per prefix (longest match precomputed)inf30s pull + pub/sub immediate
dest:group:42lista destination_id appartenenti al groupinf30s pull + pub/sub on group change
filter:activelista access_policies attiviinfpub/sub on filter change
plan:N:rulesper (plan_id, destination_id) → attempt-listinf30s pull + pub/sub on plan change
cb:terminator:Xcircuit breaker state (closed/open/half_open)60slive Kamailio counter
chan:terminator:Xactive channels countlivelive Kamailio counter
fraud:blocklist:CLIlista CLI bloccati (cap. 39 + cap. 46)infpub/sub immediate

6. Implicazioni implementative

6.1 Backend FastAPI (services/routing/)

Moduli da implementare in Fase 3a:

  • services/routing/plans_service.py — CRUD plan, list-detail (con materializzazione lazy delle destinations no-routing).
  • services/routing/attempts_service.py — gestione routing_rule_attempts + routing_attempt_trunks con validazione (max 4 slot, somma % = 100 per slot).
  • services/routing/sync_service.py — push delta verso Redis pub/sub channel akira:routing:update (Kamailio sottoscrive); pull full reload 30s come safety net.
  • services/routing/dry_run_service.py — simulazione impact su CDR ultimi 7gg per "Dry-run impact preview" button.
  • services/surcharge_filters/ — analoghi per filters: CRUD + sync Redis + dry_run + audit blocked_calls insert.
  • agent_tools/routing_tools.py — tool AgentCore per: query_plan_coverage, block_destination, add_filter (con requires_confirmation).

6.2 Frontend (apps/frontend/)

Componenti da implementare in Fase 3a:

  • RoutingPlanList — lista con coverage stats (riusa shadcn Table).
  • RoutingPlanDetail — tabella destinations con badge configured/no-routing + filtri (search, status).
  • RoutingByDestinationEditor — 4-column attempt editor (componente più complesso, ~600 righe TS). Riusa AttemptSlot × 4 + FailoverPolicySection.
  • SellingPricesModal — fetch /api/v1/destinations/:name/selling-prices e mostra customer × tariff matrix.
  • SurchargeFilterList + FilterEditor + DestinationGroupEditor.
  • BlockedCallsAuditTable — virtual scroll (>10k righe possibili).

6.3 Kamailio config (infra/roles/kamailio/templates/kamailio.cfg.j2)

Sezioni da scrivere production-ready (cap. 47.4 Fase 2):

  • request_route con dispatch step 1-5.
  • failure_route con cause matching da failover_policy.
  • branch_route per pre-INVITE outbound rewrite (number_rewrite_rules cap. 11.5).
  • htable declarations + KEMI script Python/Lua per logica complessa (preferito Python via app_python modulo).
  • acc_json config per CDR push verso NATS JetStream.
  • redis_sub + redis_pub modules per cache invalidation event-driven.

7. Cosa è esplicitamente NON gestito da questo modello (parking lot v2)

  • Time-of-day routing (cap. 18.9 — roadmap v2): cambio attempt-list per fascia oraria. Per ora il plan è statico.
  • CLI-based routing (cap. 18.9 — v2): cambio routing in base alla CLI matchata (pattern Stealth cli_orig_term_routing). Posticipato a v2.
  • ASR-based dynamic weight (cap. 18.9 — v2): aggiustamento automatico % weight su base ASR live. Pericoloso senza tooling osservabilità maturo.
  • Shadow routing / A-B testing (cap. 18.9 — v2+): traffic split per testare nuovi terminator senza impatto produzione.
  • Cost Explorer (cap. 21 — v1.1 post-pilot): UI separata per esplorare cost-by-destination cross-supplier oltre il "View Selling Prices" già implementato.
  • Multi-destination batch edit (cap. 21.5 — v2): editor matrix N destinations × M trunk con bulk operations avanzate.

8. Riferimenti incrociati

  • ADR-0001 stack Python/FastAPI/Next.js → docs/adr/0001-stack-python-fastapi-nextjs.md
  • ADR-0007 CDR pipeline NATS JetStream → docs/adr/0007-cdr-pipeline-nats-jetstream.md
  • ADR-0008 Kamailio HA su Hetzner Cloud → docs/adr/0008-kamailio-ha-hetzner-cloud.md
  • Signaling decisions consolidate → docs/architecture/signaling-decisions.md
  • Roadmap fasi 0-4 → docs/architecture/roadmap-v5.16.md
  • Schema DB completo (tabelle routing_* e access_policies) → akira_schema_v1.sql + docs/db/SCHEMA_DELTA_v515_to_v1.md
  • Pattern fraud detection (per audit blocked_calls integration) → docs/security/fraud-detection-patterns.md

9. Storia delle revisioni

DataVersioneCambio
2026-05-13 (sessione 2)1.0Prima versione consolidata dopo rifacimento mockup. Allinea modello concettuale a pattern Stealth (Routing by Destination + Surcharge Filters pass-through). Distingue chiaramente le 3 entità (Destinations / Routing Plan / Surcharge Filter) e il flow runtime Kamailio in 6 step.

Documento consolidato 2026-05-13 da Massimo Bagnoli (m.bagnoli@asheep.it). Vincolante per implementazione Fase 2 (Kamailio config) e Fase 3a (backend services + frontend pages).