Skip to content

AstraCall Engine — Build Plan (replace liblinphone, calls only)

Decision (Hari, 2026-07-03): no Belledonne commercial license. We replace the GPL liblinphone stack with our own phone-side engine talking to our existing Asterisk. Scope = one feature: highly reliable, stable calls. No chat, no meetings, no recordings tabs, no extra UI surfaces. This document is the build spec; the UAT plan's call rows are the acceptance contract.

Related: voip-push-wake.md (carries over verbatim), ios-uat-test-plan.md (parity contract), decisions.md (licence question → resolved by this plan).


1. Goal & non-goals

Goal: an iOS app whose entire job is: register to our Asterisk, ring reliably in every app/phone state, connect fast with 2-way audio that survives network changes, hang up cleanly — with zero GPL code, App-Store-submittable.

Non-goals (v1): chat/conversations, meetings, recordings browser, video, contacts sync, MD analytics/tickets (stay in the existing internal app), multi-account, custom in-call UI beyond essentials (CallKit's native UI is the primary call surface).

2. Licensing outcome

Component License Status
AstraCall engine (Swift SIP + glue) Ours, proprietary written by us
Google WebRTC (media) BSD-3 App-Store safe, attribution in About
App UI shell Ours written fresh — no upstream-derived files copied
liblinphone / linphone-iphone GPLv3 gone at cutover — L-00 gate dissolves

Rule during the build: knowledge and our design docs carry over; no Belledonne source is copied into the new target. Files we authored from scratch in the old app (AstradialMD-era screens) are ours to reuse; anything derived from upstream (call screens, TelecomManager/CoreContext glue) gets rewritten against the same behavioral spec (this wiki).

3. Architecture

┌──────────────────────────── iPhone ────────────────────────────┐
│  App shell (SwiftUI, minimal): sign-in → pick extension →      │
│  keypad · settings-lite (expiry, logout) · CallKit native UI   │
│────────────────────────────────────────────────────────────────│
│  AstraCallKit (ours): CXProvider/PushKit glue — wake v2 client │
│  (push-ack POST, placeholder call, no phantom sessions ever)   │
│────────────────────────────────────────────────────────────────│
│  AstraSIP (ours, ~3-5k lines): SIP-over-WSS — REGISTER/INVITE/ │
│  ACK/BYE/CANCEL/re-INVITE(hold)/OPTIONS · digest auth ·        │
│  dialog & transaction state machines (reliable transport →     │
│  NO UDP retransmission machinery) · reconnect w/ backoff       │
│────────────────────────────────────────────────────────────────│
│  Google WebRTC (BSD): PeerConnection audio · Opus/G711 · echo  │
│  cancel · jitter · DTLS-SRTP · ICE · DTMF (RFC4733 sender)     │
└───────────────┬────────────────────────────────────────────────┘
                │ WSS :8089 (SIP)  +  SRTP/ICE (media)
┌───────────────▼────────────── Asterisk (existing) ─────────────┐
│ transport-wss + endpoint webrtc=yes (generator flag)           │
│ EVERYTHING ELSE UNCHANGED: dialplan, wake endpoint + push-ack, │
│ queues/IVR/failover, CDR, MixMonitor recording, expiry/qualify │
└────────────────────────────────────────────────────────────────┘

Key simplifications vs liblinphone we get for free:

  • WSS = reliable transport → no SIP retransmit timers, no NAT rport dance; the TCP/TLS connection is the NAT pinhole and the server sees disconnects instantly (sharper than qualify).
  • Our push, our call objects → the entire liblinphone pushkit-phantom bug class (call-id dedup, 15s zombie placeholder, processPushNotification) is structurally impossible.
  • One server (ours) → we implement the SIP subset both ends actually speak, not the RFC universe.

4. Component specs

4.1 Server (config-only, via repo + deploy)

  • http.conf: enable TLS server :8089, Let's Encrypt cert for stagesip.astradial.com / devsip.astradial.com (certbot renew hook → module reload http).
  • pjsip: add [transport-wss] type=transport, protocol=wss.
  • userProvisioningService: new per-user flag (e.g. sip_port='wss' or a dedicated column) emits webrtc=yes endpoints (Asterisk auto-sets dtls_auto_generate_cert-style params: use webrtc=yes + dtls_cert_file from the LE cert). Dual-stack: UDP users untouched; migration is per-user.
  • Wake endpoint/dialplan/ack: zero changes (transport-agnostic).
  • Contingency (only if NET tests fail on hostile carrier NAT): coturn on the same VPS, ice_host_candidates/STUN config. Not built until proven needed.

4.2 AstraSIP (Swift package, ours)

  • Message layer: parser/serializer for the subset; strict on what we send, tolerant on what Asterisk sends. Full unit-test corpus from live Asterisk captures.
  • Transactions/dialogs: INVITE client+server transactions on reliable transport; CSeq/route/tag handling; digest auth (MD5 + SHA-256) on 401/407; REGISTER refresh at expiry/2 honoring the Settings picker (30s–5m, server floor/cap 30/300).
  • Transport: URLSessionWebSocketTask (sip subprotocol per RFC 7118), ping keepalive, exponential-backoff reconnect, refreshRegisters() equivalent on wake/foreground/network-path change (NWPathMonitor).
  • API surface mirrors the behaviors the app already depends on (the wake v2 client contract): register/unregister, call(uri), accept/terminate/ hold/resume/sendDTMF, delegate events (incoming, state, media-connected).

4.3 Media (Google WebRTC iOS)

  • Audio-only RTCPeerConnection per call; SDP offer/answer glued into INVITE/200/ACK (and re-INVITE for hold: a=sendonly/MOH from Asterisk).
  • CallKit audio discipline (the known-hard part, budgeted for on-device iteration): RTCAudioSession manual mode; activate media only in provider(didActivate:); the answer-before-INVITE deferred-accept behavior reimplemented per wake v2.
  • DTMF: RTCRtpSender.dtmfSender (RFC 4733) — matches Asterisk endpoint dtmf_mode=rfc4733.
  • Dependency: pin Google WebRTC iOS release (SPM binary, checksum-pinned; build-from-source is the later hardening option).

4.4 AstraCallKit + wake v2 client (port of validated design)

  • PushKit: on push → ① POST push-ack (push-id) before anything else, ② report CallKit call (caller number from payload), ③ connect WSS + REGISTER, ④ INVITE arrives → attach to the placeholder call (single call object — no re-keying gymnastics needed).
  • 30s no-INVITE safety end; caller-cancel handling; locked-screen answer; hangup → BYE (+ engine-level guarantee: pending actions resolve against the one live call).

4.5 App shell (fresh target AstraCall, SwiftUI, ours)

Screens, total: Sign-in (Firebase → org JWT) → Quick Connect (pick extension; fixes DEF-006 in the rewrite) → Keypad → Settings-lite (expiry picker, logout, About/licenses). In-call UX = CallKit native UI + one minimal in-app call card (mute/speaker/keypad/hold/hang up). Recents can come from the platform CDR API later — not a v1 gate.

5. Phases & gates (each phase ends demonstrably working on the iPhone)

Phase Deliverable Gate
A — Door + skeleton ✅ 07-03 staging WSS live; AstraSIP registers from the iPhone; contact visible via transport=ws REGISTER 200 + expiry honored — PASSED
B — First audio ✅ 07-03 INVITE in/out with WebRTC audio, ext↔ext on staging 2-way audio both directions — PASSED (recorded, clean hangups, zero ICE/DTLS errors)
C — CallKit + wake PushKit/CallKit port; killed/background/locked answer; wake v2 E2E (server untouched) WAKE-01..07, 13, 14 pass
D — Call features hold/resume, DTMF, transfer via server features, busy/decline paths, failover interplay OUT/INB/AUD P1 rows pass
E — Resilience WiFi↔LTE ICE restart, reconnect storms, expiry honesty, 24h soak NET rows + REG-05/06 pass
F — Ship shell polish, privacy manifest, export-compliance key, TestFlight, prod E2E, App Store Launch gates (L-01..13, no L-00)

Honest timeline: A today; B within days (audio glue is the first device-iteration loop); C–D the second week; E–F depend on soak results and review turnaround. Aggressive-realistic: 2–4 weeks to TestFlight; "today" means Phase A live today, not App Store today. Every liblinphone lesson from 2026-06-30→07-03 (this wiki) is a pre-paid shortcut — that's why weeks, not months.

6. Test strategy

  • AstraSIP unit tests: message corpus (recorded from our Asterisk), auth, dialog state machines — runs on every build, no device needed.
  • Device matrix per phase gate = the existing UAT plan rows (WAKE/OUT/INB/ AUD/NET/REG) — the old app's validated behavior is the spec.
  • Old app stays installed side-by-side during the build (different bundle id for the new target until cutover) — A/B the same scenarios.

7. Migration & cutover

Rollback safety: the working liblinphone app is frozen at tag linphone-stable-2026-07-03 / branch linphone-stable (astradial-ios, commit 259704a14) — build + reinstall it anytime if the new engine slips.

  1. Per-user dual-stack: test extensions get webrtc=yes endpoints; UDP fleet untouched.
  2. When Phase E gates pass → new target adopts bundle id com.astradial.softphone (keeps the push token/entitlements), old linphone-based app archived (repo stays for reference; never shipped).
  3. App Store submission with no GPL in the binary; L-00 gate deleted; remaining gates: privacy manifest, export key, review kit, demo account.
  4. Existing internal MD app (analytics/tickets) remains a separate internal tool — not part of this product.

8. Risks (ranked) & mitigations

  1. CallKit ↔ WebRTC audio-session timing (no-audio-on-answer class) — budgeted device-iteration loops; known playbook (manual RTCAudioSession, didActivate discipline); worst case mirrors the fixes we already shipped once this week.
  2. Hostile carrier NAT breaks ICE — coturn contingency (half-day), only if NET tests fail.
  3. WebRTC binary dependency — pin + checksum; later build-from-source.
  4. Scope creep — this doc's non-goals section is the contract; anything not "call" goes to the backlog.
  5. Asterisk WSS cert/renewal ops — certbot deploy-hook + monitoring (existing Upptime can probe :8089).

9. Day-one (Phase A) checklist

  • LE cert on stagesip + http.conf TLS + transport-wss (staging, synced back to repo per staging-direct-edit workflow)
  • Generator: webrtc=yes endpoint flag for a test user (1005-wss)
  • Repo: new AstraCall Xcode target + AstraSIP Swift package skeleton
  • WSS connect + REGISTER + 200 (digest) from the iPhone on staging
  • pjsip show contacts shows the WSS contact; wake endpoint sees it reachable (zero server logic changes)