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/planserouting/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
nameunivoco +country_iso2(derivato da E.164 cap. 56) + lista diprefixesversionati convalid_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.destinationsnel 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_policydi default (rerouteable causes Q.850 + SIP, PDD threshold, invite_timeout, max_total_attempts cap. 18.7 v5.16 = 4)circuit_breakerconfig (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/surchargea 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 perallow_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
| Slot | Nome UI | Cosa fa runtime | Trunk per slot |
|---|---|---|---|
| 1 | First attempt | Primo tentativo. Distribuisce il traffico su N trunk weighted secondo technique. | 1..N (default: 1) |
| 2 | Second attempt | Failover #1 se First attempt fallisce con SIP/Q.850 rerouteable cause. | 1..N |
| 3 | Third attempt | Failover #2. | 1..N |
| 4 | Fourth 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 persupplier_ratecrescente. 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
| Action | Effetto runtime | SIP response | Use case tipico |
|---|---|---|---|
block | Rifiuta INVITE | 403 Forbidden / 603 Decline | Anti-Wangiri/IRSF su prefissi premium-rate; protezione retail customer da destinazioni rischiose. |
allow_only | Permetti SOLO ai matched originator (whitelist), blocca tutti gli altri verso quel destination group | 403 per non-matched | Destination satellitari/iridium permesse solo a Originator con contratto premium. |
surcharge | Aggiungi +€/min al rate Tariff per chiamate matched | nessuna (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 dadestinations.tags(GIN index). Es. group "Premium-rate-blocked" = tagpremium OR iridium OR sat.manual: lista esplicita di destination ids.
access_policies(= un filter): riferimento adestination_group_id,action,surcharge(se action=surcharge),applies_to(originator tier o lista specifica).access_policy_authorized_originators: tabella N:M perallow_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 Redis | Contenuto | TTL | Refresh |
|---|---|---|---|
dest:prefix:39 | destination_id per prefix (longest match precomputed) | inf | 30s pull + pub/sub immediate |
dest:group:42 | lista destination_id appartenenti al group | inf | 30s pull + pub/sub on group change |
filter:active | lista access_policies attivi | inf | pub/sub on filter change |
plan:N:rules | per (plan_id, destination_id) → attempt-list | inf | 30s pull + pub/sub on plan change |
cb:terminator:X | circuit breaker state (closed/open/half_open) | 60s | live Kamailio counter |
chan:terminator:X | active channels count | live | live Kamailio counter |
fraud:blocklist:CLI | lista CLI bloccati (cap. 39 + cap. 46) | inf | pub/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— gestionerouting_rule_attempts+routing_attempt_trunkscon validazione (max 4 slot, somma % = 100 per slot).services/routing/sync_service.py— push delta verso Redis pub/sub channelakira: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(conrequires_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). RiusaAttemptSlot× 4 +FailoverPolicySection.SellingPricesModal— fetch/api/v1/destinations/:name/selling-pricese 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_routecon dispatch step 1-5.failure_routecon cause matching dafailover_policy.branch_routeper pre-INVITE outbound rewrite (number_rewrite_rules cap. 11.5).htabledeclarations + KEMI script Python/Lua per logica complessa (preferito Python viaapp_pythonmodulo).acc_jsonconfig per CDR push verso NATS JetStream.redis_sub+redis_pubmodules 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
| Data | Versione | Cambio |
|---|---|---|
| 2026-05-13 (sessione 2) | 1.0 | Prima 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).