From 0456d2b4ae7e9adc2cb03029d6834610332f7147 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Wed, 5 Aug 2026 14:05:22 +0800 Subject: [PATCH] Configure self-hosted STUN for remote WebRTC --- .env.example | 7 +++-- backend/.env.example | 5 ++-- backend/services/webrtc_ice.py | 6 ++--- backend/settings.py | 8 ++++-- deploy/README.md | 49 ++++++++++++++++++++-------------- docker-compose.yaml | 12 +++++---- docs/integrations.mdx | 2 +- 7 files changed, 53 insertions(+), 36 deletions(-) diff --git a/.env.example b/.env.example index 5b846be..ba9c3c5 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,10 @@ # docker compose 变量(复制为项目根 .env)。公网 WebRTC 需配合 --profile remote。 -PUBLIC_IP=182.92.86.220 +# PRIVATE_IP 是 `ip -4 route get 1.1.1.1` 输出中 src 后的地址。 +PUBLIC_IP=118.89.89.89 +PRIVATE_IP=10.0.0.8 TURN_SECRET=change-me-to-a-long-random-string -TURN_URLS=turn:182.92.86.220:3478?transport=udp,turn:182.92.86.220:3478?transport=tcp +STUN_URL=stun:118.89.89.89:3478 +TURN_URLS=turn:118.89.89.89:3478?transport=udp,turn:118.89.89.89:3478?transport=tcp S3_ACCESS_KEY=rustfsadmin S3_SECRET_KEY=rustfsadmin S3_BUCKET=ai-video diff --git a/backend/.env.example b/backend/.env.example index b2fce9a..0b78aba 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -23,7 +23,8 @@ KNOWLEDGE_TOP_K=5 # ---- WebRTC TURN(公网跨网语音预览;本地开发留空) ---- # 与 docker compose --profile remote 的 coturn 配套。云安全组需放行 UDP 3478 与 49152-49200。 -# PUBLIC_IP 填云主机公网 IP(compose 里给 coturn --external-ip 用,见项目根 .env)。 -# TURN_URLS=turn:182.92.86.220:3478?transport=udp,turn:182.92.86.220:3478?transport=tcp +# Docker Compose 部署请在项目根 .env 设置这些变量;此处适用于宿主机直接运行 uvicorn。 +# STUN_URL=stun:118.89.89.89:3478 +# TURN_URLS=turn:118.89.89.89:3478?transport=udp,turn:118.89.89.89:3478?transport=tcp # TURN_SECRET=your-turn-secret # TURN_CREDENTIAL_TTL=86400 diff --git a/backend/services/webrtc_ice.py b/backend/services/webrtc_ice.py index 307ffbe..588ff11 100644 --- a/backend/services/webrtc_ice.py +++ b/backend/services/webrtc_ice.py @@ -9,8 +9,6 @@ import time import settings -STUN_URL = "stun:stun.l.google.com:19302" - def _turn_credentials() -> tuple[str, str] | None: """Return (username, credential) for TURN, or None when TURN is not configured.""" @@ -34,7 +32,7 @@ def _turn_credentials() -> tuple[str, str] | None: def client_ice_servers() -> list[dict]: """ICE servers for browser RTCPeerConnection (JSON-serializable).""" - servers: list[dict] = [{"urls": STUN_URL}] + servers: list[dict] = [{"urls": settings.STUN_URL}] creds = _turn_credentials() if not creds: return servers @@ -48,7 +46,7 @@ def aiortc_ice_servers() -> list: """ICE servers for backend SmallWebRTCConnection / aiortc.""" from aiortc import RTCIceServer - servers = [RTCIceServer(urls=STUN_URL)] + servers = [RTCIceServer(urls=settings.STUN_URL)] creds = _turn_credentials() if not creds: return servers diff --git a/backend/settings.py b/backend/settings.py index e1f8700..7a85e5b 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -38,9 +38,13 @@ AUTH_TOKEN_EXPIRE_MINUTES = int(os.getenv("AUTH_TOKEN_EXPIRE_MINUTES", "1440")) AUTH_COOKIE_SECURE = os.getenv("AUTH_COOKIE_SECURE", "false").lower() == "true" AUTH_COOKIE_SAMESITE = os.getenv("AUTH_COOKIE_SAMESITE", "lax") -# ---- WebRTC TURN ---- +# ---- WebRTC STUN / TURN ---- +# Override STUN_URL in remote deployments to use the colocated coturn server +# instead of waiting for a public STUN service that may be unreachable. +STUN_URL = os.getenv("STUN_URL", "stun:stun.l.google.com:19302") + # TURN_URLS example: -# turn:182.92.86.220:3478?transport=udp,turn:182.92.86.220:3478?transport=tcp +# turn:118.89.89.89:3478?transport=udp,turn:118.89.89.89:3478?transport=tcp TURN_URLS = _split(os.getenv("TURN_URLS", "")) TURN_SECRET = os.getenv("TURN_SECRET", "") TURN_USERNAME = os.getenv("TURN_USERNAME", "") diff --git a/deploy/README.md b/deploy/README.md index 2202aed..b580fae 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -170,44 +170,53 @@ https://<远程机器IP或域名>:18189 如果浏览器和后端不在同一内网,WebRTC 媒体链路通常需要 TURN。项目里的 `docker-compose.yaml` 已经带了 coturn,用 `remote` profile 启动即可。 -#### 6.1 配项目根目录 `.env` +#### 6.1 确认公网/私网 IP -项目根目录的 `.env` 给 coturn 容器用: +查询云主机默认路由使用的私网 IP: + +```bash +ip -4 route get 1.1.1.1 +``` + +输出中 `src` 后的地址是 `PRIVATE_IP`。例如: + +```text +1.1.1.1 via 10.0.0.1 dev eth0 src 10.0.0.8 +``` + +云控制台中绑定到该主机、用于 SSH 的公网地址是 `PUBLIC_IP`。 + +#### 6.2 配项目根目录 `.env` + +Docker Compose 会把项目根 `.env` 中的 ICE 配置同时传给 API 和 coturn: ```text PUBLIC_IP=<远程机器公网IP> +PRIVATE_IP=<远程机器私网IP> TURN_SECRET=<一串足够长的随机密钥> +STUN_URL=stun:<远程机器公网IP>:3478 +TURN_URLS=turn:<远程机器公网IP>:3478?transport=udp,turn:<远程机器公网IP>:3478?transport=tcp ``` 示例: ```text PUBLIC_IP=118.89.89.89 +PRIVATE_IP=10.0.0.8 TURN_SECRET=change-me-to-a-long-random-string -``` - -#### 6.2 配后端 `backend/.env` - -后端用同一个 `TURN_SECRET` 签发短时 TURN 凭证: - -```text -TURN_URLS=turn:<远程机器公网IP>:3478?transport=udp,turn:<远程机器公网IP>:3478?transport=tcp -TURN_SECRET=<和项目根 .env 一致的密钥> -``` - -示例: - -```text +STUN_URL=stun:118.89.89.89:3478 TURN_URLS=turn:118.89.89.89:3478?transport=udp,turn:118.89.89.89:3478?transport=tcp -TURN_SECRET=change-me-to-a-long-random-string ``` -改完 `backend/.env` 后,重启后端 `uvicorn`。 +`STUN_URL` 指向同机 coturn,可避免后端等待不可达的公共 STUN。 +Compose 的 `environment` 会覆盖 `backend/.env` 中的同名值,因此 Docker +部署必须在项目根 `.env` 设置上述变量。 #### 6.3 启动 coturn ```bash -docker compose --profile remote up -d coturn +docker compose config --quiet +docker compose --profile remote up -d --build --force-recreate api coturn ``` 查看状态和日志: @@ -259,7 +268,7 @@ curl -k https://<远程机器IP或域名>:18189/api/webrtc/ice-servers ```json { "iceServers": [ - {"urls": "stun:stun.l.google.com:19302"}, + {"urls": "stun:118.89.89.89:3478"}, { "urls": "turn:<远程机器公网IP>:3478?transport=udp", "username": "...", diff --git a/docker-compose.yaml b/docker-compose.yaml index cbcf703..875d3b0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -49,7 +49,8 @@ services: DATABASE_URL: "postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/postgres" # 3030 = docker ui 宿主端口;3000 = 宿主机裸跑 npm run dev 时的端口 CORS_ORIGINS: "http://localhost:3030,http://127.0.0.1:3030,http://localhost:3000,http://127.0.0.1:3000" - # WebRTC TURN(公网部署:设 PUBLIC_IP + TURN_SECRET,并 docker compose --profile remote up) + # WebRTC ICE(公网部署:STUN 指向同机 coturn,避免等待不可达的公共 STUN) + STUN_URL: "${STUN_URL:-stun:stun.l.google.com:19302}" TURN_URLS: "${TURN_URLS:-}" TURN_SECRET: "${TURN_SECRET:-}" S3_ENDPOINT_URL: "http://rustfs:9000" @@ -118,7 +119,7 @@ services: networks: [app-network] # ---- 可选(profile: remote):WebRTC 公网穿透 ---- - # 在项目根 .env 设置 PUBLIC_IP(云主机公网 IP)与 TURN_SECRET,与 backend TURN_SECRET 一致。 + # 在项目根 .env 设置 PUBLIC_IP、PRIVATE_IP 与 TURN_SECRET。 # 云安全组放行:UDP/TCP 3478,UDP 49152-49200。 coturn: image: coturn/coturn:4.8.0 @@ -128,11 +129,12 @@ services: - -n - --log-file=stdout - --listening-port=3478 - - --listening-ip=0.0.0.0 - - --external-ip=${PUBLIC_IP:?set PUBLIC_IP in .env for coturn} + - --listening-ip=${PRIVATE_IP:?set PRIVATE_IP in .env for coturn} + - --relay-ip=${PRIVATE_IP:?set PRIVATE_IP in .env for coturn} + - --external-ip=${PUBLIC_IP:?set PUBLIC_IP in .env for coturn}/${PRIVATE_IP:?set PRIVATE_IP in .env for coturn} - --realm=ai-video - --use-auth-secret - - --static-auth-secret=${TURN_SECRET:-changeme} + - --static-auth-secret=${TURN_SECRET:?set TURN_SECRET in .env for coturn} - --min-port=49152 - --max-port=49200 - --no-cli diff --git a/docs/integrations.mdx b/docs/integrations.mdx index 55223a3..6552c36 100644 --- a/docs/integrations.mdx +++ b/docs/integrations.mdx @@ -56,4 +56,4 @@ icon: plug {"assistant_id":"asst_xxx"} ``` -WebRTC 在公网环境通常需要 TURN。设置 `TURN_URLS` 和 `TURN_SECRET` 后,可通过 `GET /api/webrtc/ice-servers` 获取浏览器 ICE 配置。 +WebRTC 在公网环境通常需要 TURN。设置 `STUN_URL`、`TURN_URLS` 和 `TURN_SECRET` 后,可通过 `GET /api/webrtc/ice-servers` 获取浏览器 ICE 配置。远程部署建议让 `STUN_URL` 指向同机 coturn,避免等待不可达的公共 STUN。