v0.46.0

thevoiceguy/siphon-aiv0.46.0Jul 28, 2026by github-actions[bot]

Upgrading?

Release notes summary

Adds TLS SNI configuration and fixes inbound TLS leg routing; introduces strict config validation.

Key Highlights

  • Added `[sip].tls_server_name` for handling IP-literal SNI scenarios.
  • Fixed inbound TLS leg routing after carrier idle-close.
  • BREAKING: Unknown keys in route config now cause startup errors.

Breaking Changes

  • Unknown keys inside `[[route]]`, `[route.match]`, or any `[route.*]` override block now cause startup errors instead of being silently ignored.

New Features

  • New `[sip].tls_server_name` configuration option
  • Strict config validation for route blocks

Full Release Notes

### Fixed

- **In-dialog requests on an inbound TLS leg re-dialed after a carrier idle-close now reach the right hop and pass certificate verification** (fixed upstream by [siphon-rs#78](https://github.com/thevoiceguy/siphon-rs/pull/78) and [siphon-rs#79](https://github.com/thevoiceguy/siphon-rs/pull/79), pin bumped `4e7d5d7d0bd7` → `376d0e9a5c5e`). Two defects sat under 0.45.3's teardown-on-FIN failover. First, `sip-dialog` unconditionally reversed the Record-Route set — correct only for the UAC side (RFC 3261 §12.1.2); a UAS takes the set as received (§12.1.1) — so the fallback dial targeted the *far end* of the proxy chain (often a portless bottom entry resolving to `:5060`) instead of the top hop's `:5061;transport=tls`. Second, a fresh TLS dial to a hop the carrier Record-Routed **by IP** used that IP literal as the SNI/reference identity, and carrier certs carry no IP SANs, so verification failed. Upstream added `UACConfig.tls_server_name` to carry the trunk hostname as the reference identity — applied only where the SNI would otherwise be an IP literal, never overriding hostname targets or non-TLS transports.
- **The `[sip].tcp_keepalive_interval_secs` keepalive now sends the RFC 5626 §3.5.1 double-CRLF ping instead of a bare CRLF** (fixed upstream by [siphon-rs#80](https://github.com/thevoiceguy/siphon-rs/pull/80)). The single CRLF is the *pong*, and emitting it unsolicited toward the server made Twilio Secure Trunking reap idle connections **sooner** (~82–88 s vs ~127 s with the knob off) — the 0.45.3 feature pushed traffic onto the recovery path more often instead of keeping connections open. CONFIG.md now also carries the honest caveat: a carrier whose idle timer counts *SIP messages* rather than bytes cannot be held open at this layer — measure the connection lifetime with the knob on before relying on it.

### Added

- **`[sip].tls_server_name` — TLS reference identity for client-side dials whose SNI would otherwise be an IP literal** (the siphon-ai half of siphon-rs#79). The daemon-wide transfer UAC drives every in-dialog request an inbound leg sends (hold re-INVITE, REFER, BYE) and has no trunk config to derive a hostname from, so the scenario that actually hits the IP-SNI failure — an inbound Twilio TLS leg re-dialed after an idle-close — needs this knob set to the trunk's hostname (e.g. `example.pstn.twilio.com`). Validated at load (must be a DNS name — an IP literal or empty value refuses to start), logged at startup when set. **Gateways and registrations need no configuration:** a `[[gateway]]`'s `proxy` hostname and a `[[register]]`'s server hostname are passed through as the reference identity automatically (an IP-literal `proxy` has no name to offer and is left unset). Documented in CONFIG.md.

### Changed

- **BREAKING (config): an unknown key inside `[[route]]`, `[route.match]`, or any `[route.*]` override block is now a startup error instead of being silently ignored** (issue #383). Every section in `crates/config` already carried `#[serde(deny_unknown_fields)]` — its doc comment calls that "the right strictness — it catches typos like `auido_sample_rate`" — but no struct in `crates/routes` did, leaving the dialplan the laxest part of the config. Two ways that bit: a misspelled match key was **dropped**, so a route written as "`from_user` **and** `to`" matched on `from_user` alone and, since routes are first-match-wins, quietly stole calls meant for later routes; and a misspelled override key (`ws_uri` for `ws_url`) was indistinguishable from "not overridden", so the route silently bridged to the global `[bridge].ws_url` — a production misroute with a clean `config OK`. The same shape applied to `[route.media]`/`[route.security]`/`[route.recording]`, where a dropped override silently inherits a *less* restrictive global. Unknown keys now produce the same "unknown field `x`, expected one of …" error the rest of the config gives, which for `to` → `to_user` is self-explaining. Unknown **top-level** tables stay tolerated as before (`RawRouteFile` is deliberately lenient — `load_from_toml` is handed whole config files and picks the routes out), and `[route.match.header]` names are map keys, so arbitrary header names are unaffected. **Upgrade note:** a deployed config carrying a stray key in a route block will now refuse to start; run `siphon-ai --config <file> check` before upgrading — the error names the offending key. The shipped `examples/twilio-trunk/siphon-ai.toml` carried exactly this bug (`register_source = "twilio"` at route level, where it did nothing — and `"twilio"` was the `[[trunk]]` name, not the `"trunk"` literal a UAS-mode call actually carries); it has been corrected.

---

**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.46.0` (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