Skip to content

feat: the quick start brings up the call relay, so calls work on a fresh clone - #2

Merged
olavgg merged 1 commit into
mainfrom
feat/quickstart-calls
Aug 1, 2026
Merged

feat: the quick start brings up the call relay, so calls work on a fresh clone#2
olavgg merged 1 commit into
mainfrom
feat/quickstart-calls

Conversation

@olavgg

@olavgg olavgg commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Calls shipped switched off for anyone following the quick start. coturn sat behind a calls profile, so podman compose up -d skipped it, and with no relay configured the app hides the call buttons entirely — correct behaviour (force-relay means no relay is no media path) but it left the feature invisible to every reader who did not go looking for a second command.

coturn now starts with the rest of the stack and the two ICHAT_TURN_* values join the existing KEYCLOAK_CLIENT_SECRET line, so the quick start is still five commands. The env vars keep their lack of defaults: CallProperties.isConfigured() stays the gate, and production still has to opt in deliberately rather than inherit a shared secret from a sample file.

Two things had to change for the default to be safe and to work at all.

The relay binds to loopback. --listening-ip was 0.0.0.0, which was defensible while a profile kept the container off; starting it by default with --static-auth-secret=dev-turn-secret in a public repository would put an open TURN relay with a known password on every interface of every machine that runs the quick start, and on a cloud VM that is a public one. All three addresses now share ICHAT_TURN_RELAY_IP and default to 127.0.0.1, which is also all the quick start needs: getUserMedia requires a secure context, localhost is the only exempt origin, so two browsers on the host are the only clients that can place a call regardless.

--allow-loopback-peers is required and is why this was tested rather than assumed. coturn refuses loopback peer addresses out of the box. In a relayed 1:1 call each browser allocates on coturn and then asks permission to send to the other allocation, so with --relay-ip=127.0.0.1 the peer address is loopback and the permission is refused — 403 Forbidden IP, after which the call rings, is answered, and silently never connects, looking exactly like a network fault. The earlier end-to-end run never hit this because it used a LAN ICHAT_TURN_RELAY_IP, where peers are LAN addresses and the rule does not apply. The flag is safe only because the relay accepts nothing but loopback clients, who can already reach loopback directly; exposing the service means dropping it, or remote clients gain a route into this host's own 127.0.0.1. The service comment says so.

Verified: podman compose config renders coturn with no profile; the recreated container listens on 127.0.0.1:3478 TCP and UDP and nowhere else; turnutils_uclient -W dev-turn-secret completes an allocation, creates a permission and relays 4 packets each way with 0 lost, which exercises the same REST-API credential scheme TurnCredentialService mints. The 403 above is the observed failure before the flag, not a predicted one.

Not covered: scripts/e2e-call.py was not re-run. It needs :8080, which an app instance already held, and the realm whitelists redirect URIs only for :8080 — so a second instance on another port cannot complete login. The browser half of this path is therefore unverified against the loopback relay, though the transport half above is the part the change touched.

Docs follow the same shape throughout: README, landing page, QUICKSTART-COMPOSE.md, .env.example and the operations page in docs.html, plus the --profile calls instructions in e2e-call.py and the screenshot recipe, which no longer name a profile that does not exist.

…esh clone

Calls shipped switched off for anyone following the quick start. coturn sat behind a `calls`
profile, so `podman compose up -d` skipped it, and with no relay configured the app hides the call
buttons entirely — correct behaviour (force-relay means no relay is no media path) but it left the
feature invisible to every reader who did not go looking for a second command.

coturn now starts with the rest of the stack and the two `ICHAT_TURN_*` values join the existing
`KEYCLOAK_CLIENT_SECRET` line, so the quick start is still five commands. The env vars keep their
lack of defaults: `CallProperties.isConfigured()` stays the gate, and production still has to opt
in deliberately rather than inherit a shared secret from a sample file.

Two things had to change for the default to be safe and to work at all.

The relay binds to loopback. `--listening-ip` was `0.0.0.0`, which was defensible while a profile
kept the container off; starting it by default with `--static-auth-secret=dev-turn-secret` in a
public repository would put an open TURN relay with a known password on every interface of every
machine that runs the quick start, and on a cloud VM that is a public one. All three addresses now
share `ICHAT_TURN_RELAY_IP` and default to `127.0.0.1`, which is also all the quick start needs:
`getUserMedia` requires a secure context, `localhost` is the only exempt origin, so two browsers on
the host are the only clients that can place a call regardless.

`--allow-loopback-peers` is required and is why this was tested rather than assumed. coturn refuses
loopback peer addresses out of the box. In a relayed 1:1 call each browser allocates on coturn and
then asks permission to send to the *other* allocation, so with `--relay-ip=127.0.0.1` the peer
address is loopback and the permission is refused — 403 Forbidden IP, after which the call rings,
is answered, and silently never connects, looking exactly like a network fault. The earlier
end-to-end run never hit this because it used a LAN `ICHAT_TURN_RELAY_IP`, where peers are LAN
addresses and the rule does not apply. The flag is safe only because the relay accepts nothing but
loopback clients, who can already reach loopback directly; exposing the service means dropping it,
or remote clients gain a route into this host's own 127.0.0.1. The service comment says so.

Verified: `podman compose config` renders coturn with no profile; the recreated container listens
on 127.0.0.1:3478 TCP and UDP and nowhere else; `turnutils_uclient -W dev-turn-secret` completes an
allocation, creates a permission and relays 4 packets each way with 0 lost, which exercises the
same REST-API credential scheme `TurnCredentialService` mints. The 403 above is the observed
failure before the flag, not a predicted one.

Not covered: `scripts/e2e-call.py` was not re-run. It needs :8080, which an app instance already
held, and the realm whitelists redirect URIs only for :8080 — so a second instance on another port
cannot complete login. The browser half of this path is therefore unverified against the loopback
relay, though the transport half above is the part the change touched.

Docs follow the same shape throughout: README, landing page, `QUICKSTART-COMPOSE.md`, `.env.example`
and the operations page in `docs.html`, plus the `--profile calls` instructions in `e2e-call.py` and
the screenshot recipe, which no longer name a profile that does not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Olav Gjerde <olav@intellistream.ai>
@olavgg
olavgg merged commit f7eb211 into main Aug 1, 2026
1 check passed
@olavgg
olavgg deleted the feat/quickstart-calls branch August 1, 2026 10:56
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.

1 participant