Skip to content

Add optional Cloudflare TURN credential fetching#19650

Open
kakahu2015 wants to merge 6 commits intoelement-hq:developfrom
kakahu2015:kakahu/cloudflare-turn-18472
Open

Add optional Cloudflare TURN credential fetching#19650
kakahu2015 wants to merge 6 commits intoelement-hq:developfrom
kakahu2015:kakahu/cloudflare-turn-18472

Conversation

@kakahu2015
Copy link
Copy Markdown

@kakahu2015 kakahu2015 commented Apr 4, 2026

Summary

This adds optional support for fetching short-lived TURN credentials from Cloudflare Realtime TURN via Synapse's existing /_matrix/client/*/voip/turnServer endpoint.

Clients do not need any Cloudflare-specific changes: Synapse performs the Cloudflare API request server-side and returns the usual Matrix TURN response shape (username, password, ttl, uris).

Why

Today Synapse supports:

  • shared-secret TURN credentials (turn_shared_secret)
  • static username/password TURN credentials (turn_username / turn_password)

That works well for self-hosted TURN servers such as coturn or eturnal, but it does not cover providers like Cloudflare Realtime TURN, where Synapse needs to call an API to mint short-lived credentials.

This is useful for deployments that want:

  • globally distributed TURN ingress without operating their own TURN fleet
  • a managed TURN service with short-lived credentials
  • lower operational overhead while keeping the existing Matrix client flow

What this PR changes

New configuration

Adds optional Cloudflare-specific settings:

  • turn_cloudflare_enabled
  • turn_cloudflare_key_id
  • turn_cloudflare_api_token
  • turn_cloudflare_api_token_path
  • turn_cloudflare_api_base_url

Adds optional broker-specific settings for federated deployments:

  • turn_federation_deployment
  • turn_broker_url
  • turn_broker_api_token
  • turn_broker_api_token_path

Adds a turn_mode setting to explicitly select the TURN credential source:

  • coturn (default): use local CoTURN directly. Backward compatible — no behaviour change if unset.
  • cf: fetch credentials from Cloudflare Realtime TURN.
  • broker: fetch credentials from a federated TURN broker.

Request flow

When turn_mode: cf is configured:

  1. Synapse receives /_matrix/client/*/voip/turnServer
  2. Synapse calls Cloudflare's TURN credential API server-side
  3. Synapse converts the Cloudflare response into the existing Matrix TURN response format
  4. Clients continue to consume the standard Matrix endpoint unchanged

When turn_mode: broker is configured:

  1. Synapse receives /_matrix/client/*/voip/turnServer
  2. Synapse calls a configured TURN broker instead of calling Cloudflare directly
  3. The broker returns the same Matrix TURN response shape (username, password, ttl, uris)
  4. Clients continue to consume the standard Matrix endpoint unchanged

When turn_mode: coturn (default) is configured:

  1. Synapse uses the existing turn_uris / turn_shared_secret or turn_username / turn_password flow as before.
  2. This is fully backward compatible.

Port 53 filtering

Cloudflare documents that alternate port 53 TURN URLs may time out in browsers.
This PR filters turn: / turns: URLs on port 53 before returning credentials to clients, while preserving the primary ports (3478, 80, 443, 5349).

Security / operational notes

  • The Cloudflare API token can be loaded from turn_cloudflare_api_token_path, which is the recommended deployment mode.
  • The broker token can be loaded from turn_broker_api_token_path, which is the recommended deployment mode for broker deployments.
  • The implementation keeps both tokens on the server side; they are never exposed to Matrix clients.
  • turn_user_lifetime continues to control the returned TTL and is reused for Cloudflare or broker credential requests.

Documentation

This PR updates:

  • TURN configuration reference
  • TURN how-to documentation

The docs explicitly describe:

  • the server-side-only nature of the integration
  • recommended use of turn_cloudflare_api_token_path
  • filtering of Cloudflare's alternate port 53 TURN URLs

Tests

Adds coverage for:

  • parsing Cloudflare TURN responses
  • validating broker TURN responses
  • filtering port 53 TURN URLs
  • rejecting mismatched multiple credential sets
  • successful Cloudflare-backed responses
  • successful broker-backed responses in federated mode
  • ignoring broker settings unless federated mode is explicitly enabled
  • config loading / secret file handling for the new options

Example config

Cloudflare TURN mode

turn_mode: cf
turn_cloudflare_enabled: true
turn_cloudflare_key_id: YOUR_CLOUDFLARE_TURN_KEY_ID
turn_cloudflare_api_token_path: /path/to/turn-cloudflare-api-token
turn_user_lifetime: 1h

Federation broker mode

turn_mode: broker
turn_federation_deployment: true
turn_broker_url: https://turn-broker.example.com/credentials
turn_broker_api_token_path: /path/to/turn-broker-api-token
turn_user_lifetime: 1h

Local CoTURN only (default, backward compatible)

# turn_mode: coturn  (optional, this is the default)
turn_uris:
  - turn:coturn.example.com:3478?transport=udp
turn_shared_secret: YOUR_SECRET
turn_user_lifetime: 1h

Reference TURN broker implementations

Two reference implementations are provided:

@kakahu2015 kakahu2015 requested a review from a team as a code owner April 4, 2026 02:26
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 4, 2026

CLA assistant check
All committers have signed the CLA.

Add a new voip.turn_mode config option that allows explicit selection
of the TURN credential source:

- coturn: Direct local CoTURN (default, backward compatible)
- cf: Cloudflare TURN with CoTURN fallback
- broker: Federation broker with CoTURN fallback

Previously the routing was implicit based on turn_federation_deployment
and turn_cloudflare_enabled flags in an if/elif relationship, which
meant enabling federation mode silently disabled Cloudflare TURN.
The new turn_mode flag makes the intent explicit and ensures each mode
has a clear fallback to local CoTURN on failure.
- turn-howto.md: document turn_mode (coturn/cf/broker) with examples
- config_documentation.md: add turn_mode reference entry
- clarify that turn_cloudflare_enabled/turn_federation_deployment
  must be set when using cf/broker modes respectively
Add turn_mode to override_config in CF and broker tests so they
exercise the correct code path with the new three-way routing.
Copy link
Copy Markdown
Contributor

@reivilibre reivilibre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for sending this in

Comment on lines +2611 to +2612
- `cf`: fetch credentials from Cloudflare Realtime TURN, falling back to local CoTURN on failure.
- `broker`: fetch credentials from a federated TURN broker, falling back to local CoTURN on failure.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback logic feels a bit surprising and maybe unnecessarily complicated; is it likely to be a real use case when people want to have both?

---
### `turn_broker_url`

*(string|null)* The URL of a TURN broker endpoint that returns Matrix-style TURN credentials (`username`, `password`, `ttl`, `uris`). This is only used when `turn_federation_deployment` is true. Defaults to `null`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am somewhat wondering what the point of a TURN broker is; what does it provide over configuring a different route in your reverse proxy to handle the TURN server credentials endpoint?

Comment thread docs/turn-howto.md
* [coturn](setup/turn/coturn.md)
* [eturnal](setup/turn/eturnal.md)

Synapse can also fetch short-lived credentials from Cloudflare Realtime TURN.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we link to some docs about this service?

Comment thread docs/turn-howto.md
Comment on lines +78 to +80
If you operate multiple federated homeservers and want them to fetch the same
shared TURN credentials for a short time window, configure those homeservers to
use a TURN broker instead of calling Cloudflare directly:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused about this; what would be the reason you'd want multiple servers with the same credentials?

Comment thread docs/turn-howto.md
Comment on lines +95 to +97
Cloudflare may return alternate port 53 TURN URLs in addition to the primary
ports. Synapse filters those `:53` TURN URLs before returning credentials to
clients, since browsers often time out on that port.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds fairly suspicious; it'd be really good to have some justification and references to back this up, if you don't mind

from tests.unittest import override_config


class ParseCloudflareTurnResponseTestCase(unittest.TestCase):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these tests should have docstrings explaining what they are testing, please :)

self.auth = hs.get_auth()
self.http_client: SimpleHttpClient = hs.get_proxied_http_client()

async def _get_turn_broker_credentials(self, ttl: int) -> JsonDict | None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these new methods should have docstrings; imagining I'm 100% unfamiliar with what these things are, right now it's a bit hard to understand what these are for

default: true
examples:
- false
turn_cloudflare_enabled:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if nesting all the config under turn_cloudflare: would make a more sensible config. I don't blame you for matching what was already there, but over the years we've started organising config a bit more hieararchically. Should make it easier to discover the relevant options, particularly in the config manual

@kakahu2015
Copy link
Copy Markdown
Author

kakahu2015 commented Apr 10, 2026

Thanks, that’s a fair point.

The code path is intended to leave the existing coturn behavior unchanged, and treat Cloudflare TURN / broker as optional best-effort fetch modes. If the fetch fails, Synapse falls back to the locally configured TURN server.

I’ll rewrite the docs and config descriptions to make that explicit, especially around when each mode is active and what the fallback behavior is. I’ll also add a clearer justification for filtering Cloudflare’s TURN URLs.

@kakahu2015
Copy link
Copy Markdown
Author

Let me clarify the broker use case, since I did not explain it well in the docs.

The TURN credential problem in federation comes down to ICE candidate matching:

  1. Traditional coturn — all homeservers point to the same coturn instance with a shared secret. No problem.

  2. Cloudflare TURN — two sub-cases:

    • Same-domain calls (Alice and Bob both on homeserver A): one server, one CF credential request, works fine.
    • Federated calls (Alice on homeserver A, Bob on homeserver B): each homeserver independently calls the Cloudflare API and gets different TURN URIs and credentials. ICE cannot form candidate pairs across different TURN relays — the call silently fails.

The broker solves case 2b: it acts as a single credential authority that fetches ONE set of CF TURN credentials and serves the same credentials to all federated homeservers. Both sides relay through the same TURN infrastructure, ICE matches, the call works.

A reverse proxy cannot do this because it has no concept of credential synchronization — it just routes requests, it cannot ensure two independent Synapse instances receive identical credentials within the same time window.

I will rewrite the docs to make this explicit.

…icate test key

- Replace substring-based port 53 check with urlparse exact port match
  to avoid false-matching port 5349 (TURNS)
- Add test case for port 5349 preservation
- Add docstrings to all new methods and tests per review feedback
- Remove duplicate turn_cloudflare_enabled key in fallback test config
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants