v0.48.5

thevoiceguy/siphon-aiv0.48.5Aug 5, 2026by github-actions[bot]

Upgrading?

AI Summary

Fixes SIP authentication nonce reuse handling and config validation typos, while ensuring all metrics have HELP lines.

Key Highlights

  • Honest peer nonce reuse no longer flagged as credential attack (now `stale`)
  • Config validation for `[route.media].dtmf` and `[route.media].srtp` typos
  • All metrics now scrape with `# HELP` lines
  • All histograms now render buckets

Full Release Notes

### Fixed

- **An honest peer reusing a still-valid nonce no longer reads as a credential attack** (issue #430). Upstream `sip-auth` rejects a correct digest over a known, unexpired nonce as soon as more than 10 s (its `max_request_age`) has passed since that nonce's last successful authentication — *before* the response digest is ever computed — and that rejection collapsed into the same `Ok(false)` as a wrong password. SiphonAI therefore answered `401 stale=false` and scored `siphon_ai_sip_auth_total{result="failed"}` plus a `sip_auth{result:"failed"}` audit event, byte-identical to a bad-credential attempt, for any UAC that caches credentials and authenticates pre-emptively (PJSIP-based UAs by default) whenever its calls are more than 10 s apart. Any SIEM rule counting `failed` events fired on honest traffic. Via a siphon-rs bump ([thevoiceguy/siphon-rs#87](https://github.com/thevoiceguy/siphon-rs/pull/87) — a `nonce_reuse_expired` discriminator in `nonce_is_stale`'s post-hoc-query style), the rejection is now mapped where RFC 7616 §3.5 points: a `401 stale=true` re-challenge (nonce unacceptable, credentials not implicated — the peer retries silently against the fresh nonce), scored and audited `stale`. `failed` now means exactly a credential mismatch. The two nonce windows also become configurable — `[sip.auth].nonce_ttl_secs` (default 300, must be ≥ 1) and `[sip.auth].nonce_reuse_window_secs` (default 10; `0` disables the reuse window so nonce reuse is bounded only by the TTL, for operators who'd rather skip the extra 401 round-trip entirely) — previously both were hardwired upstream defaults. Documented in CONFIG.md (`[sip.auth]`) and DEPLOY.md (metric semantics).

- **`[route.media].dtmf` and `[route.media].srtp` typos now fail startup instead of silently inheriting the global.** Both route overrides were accepted unvalidated: `dtmf = "of"` (or any unknown token) loaded fine and behaved as "inherit the global RFC-2833 payload type" — only an exact `"off"` disabled telephone-events — and an unknown `srtp` token loaded and warn-fell-back to the daemon default on every call. Config load now checks both against the same token sets as the global fields (`"rfc2833" | "off"`, `"off" | "preferred" | "required"`) and rejects anything else, matching the existing load-time validation for route `vad`, `recording.mode`, `min_attestation`, and `on_ws_failure`. **Upgrade note:** a config that today carries a misspelled value in either field will stop loading — run `siphon-ai check` before upgrading production, same drill as the 0.46.1 strict route keys (#384). Documented in CONFIG.md.

- **Every metric now scrapes with a `# HELP` line, and every histogram renders buckets** (issue #431). Thirteen metric families emitted from sip-glue, media-glue, quality, and the daemon binary had a DEPLOY.md row apiece but no `describe_*!` registration, so they exported as a bare `# TYPE` with no description — found live when `siphon_ai_sip_auth_total` exported four `result` series and explained none of them. Four `rtp_*` quality histograms were also missing from the bucket registrations, so they rendered as Prometheus summaries (quantiles), which cannot be aggregated across instances — while their sibling `rtp_rtt_ms`, recorded three lines away in the same match arm, had both. Buckets: jitter / RX-jitter 1 ms–500 ms (one frame time is 20 ms), packet loss across the full 0.0–1.0 fraction, MOS cut on the conventional quality bands (2.6/3.1/3.6/4.0). The gap was silent because coverage was a hand-written list of seven metrics; it is now self-detecting — `ALL_COUNTERS` / `ALL_GAUGES` / `ALL_HISTOGRAMS` name every metric the telemetry module declares, one test renders each and requires a HELP line, one requires every histogram to render buckets rather than quantiles, and one scans the module's own source so a new metric const that isn't listed fails the build instead of quietly escaping coverage.

### Documentation

- **Stale "deferred" notes swept.** A code-review pass found doc comments still describing shipped work as pending: the acceptor's module header claimed BYE/CANCEL plumbing, CDR/webhook emission, and config-driven `forward_headers` were unwired (all landed with the daemon runtime); the runtime's deferred list still included outbound REGISTER, HEP/Homer, and the admin endpoints; the call controller claimed `clear`/`mark`/`transfer`/`send_dtmf` were logged-only; the webhook schema claimed HMAC signing was a follow-up (`X-SiphonAI-Signature` ships since 0.11.0); the raw config called `recording.mode = "on_demand"` "a later chunk" (shipped 0.5.0); and PROTOCOL.md §3.8 + the WS schema still said `rtp_stats.rtcp_rtt_ms` is `null` "until forge originates its own RTCP SRs (deferred to 0.3.1)" — it populates since 0.3.2. Comments and docs now describe what's actually built; no behavior change.

---

**Artifacts**

- Static musl binaries for `x86_64` and `aarch64` (`.tar.gz`).
- Debian packages (`.deb`) for `amd64` and `arm64` — `apt install ./siphon-ai_*_amd64.deb`.
- `SHA256SUMS` — verify with `sha256sum -c SHA256SUMS`.
- `*-sbom.cdx.json` — CycloneDX SBOM.
- `SHA256SUMS.cosign.bundle` — cosign keyless signature over the checksums.
- Container: `ghcr.io/thevoiceguy/siphon-ai:v0.48.5` (multi-arch, cosign-signed).

Verify the checksums signature:
```
cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS
```

Related Repositories