跳到正文

部署网络拓扑与配置手册 ​

本文是跨项目部署网络的主文档,面向部署工程师、运维人员和排查跨主机链路的开发人员。 示例使用符号化主机名和占位地址;附录单独记录当前 aliyun + debian12-show + FRP 拓扑。本文只给出网络相关片段,不替代完整配置文件,也不保存真实密码、token、证书私钥或 shared secret。

默认端口以服务目录为唯一来源,字段语义以各模块配置文档为准:

1. 网络模型 ​

1.1 三类地址不能混用 ​

地址类型含义典型字段判断方法
监听 / bind进程实际绑定的本机网卡和端口server.host、listenHost、listen-ip、sip-ip、rtp-ip在服务所在主机执行 ss -lntup 能看到
连接 / upstream调用方用于建立连接的地址freeswitch.host、audioFork.wsUrl、TTS endpoint、数据库 URL从调用方主机能够解析、路由并完成握手
公开 / advertised返回给另一节点或写入 SIP/SDP、ICE、媒体响应的地址callbackHost、TTS publicBaseUrl、external_sip_ip、external_rtp_ip、录音 URL、TURN external-ip从最终使用该地址的节点能够访问

0.0.0.0 只适合作为监听地址,不能作为 upstream 或公开地址。127.0.0.1 永远指向当前 进程所在主机:只有调用方和目标服务在同一网络命名空间时才可使用。NAT 主机通常用私网 IP 绑定,用公网 IP 或域名广告。

1.2 完整拓扑 ​

1.3 呼叫控制时序 ​

外呼由 callout-server 或管理 API 发起时,控制方向是调用方先连接 FreeSWITCH inbound ESL; 接通后 FreeSWITCH 再连接该 cell 配对的 callflow-esl outbound socket。两条 ESL 连接用途不同, 不能只开放其中一条。

1.4 WebRTC 媒体流 ​

WSS 只承载 SIP 信令,不承载 RTP。浏览器显示 WSS 已连接不代表媒体可用;必须继续检查 ICE candidate、DTLS 状态和双方 RTP 计数。

1.5 逐连接矩阵 ​

下表的自研服务端口均引用服务目录。生产部署可以覆盖端口, 但源、目标、防火墙和反向代理必须作为一组同步修改。

源目标协议 / 默认端口长连接主要配置公网开放检查
浏览器Nginx Web/APIHTTPS 443否VITE_*_API_BASE_URL、Nginx location是页面与 GET /api/...
浏览器Nginx → FreeSWITCHSIP over WSS 443 → 7443是Sofia wss-binding、Nginx Upgrade仅 Nginx 入口SIP REGISTER
浏览器FreeSWITCH VertoJSON-RPC over WS/WSS 8081 / 8082是verto.conf.xml、WebRTC 视频通话阿里云节点已按 /verto 反代到明文 8081Verto Login 与视讯流
SIP trunk / 终端FreeSWITCHSIP UDP/TCP/TLS,现场定义是Sofia profile、gateway、realm按来源限制sofia status
浏览器coturnSTUN/TURN UDP+TCP 3478会话期两个后端 turn.*是authenticated allocation
浏览器 / coturnFreeSWITCHDTLS-SRTP / RTP,现场范围会话期external_rtp_ip、switch.conf.xml是,UDP 范围getStats()、RTP 计数
coturn浏览器 / FreeSWITCHTURN relay UDP,独立范围会话期min-port / max-port是,UDP 范围relay address
FreeSWITCHcallflow-esloutbound ESL TCP 9911通话期dialplan socket、esl-server.*否建立真实呼叫
callflow-eslFreeSWITCHinbound ESL TCP 8021,部署样例另有覆盖常驻 / 命令期freeswitch.*、event_socket.conf.xml否auth、api status
callout-server每个 FreeSWITCHinbound ESL TCP常驻 / 命令期cells[].freeswitch.*否事件监听与 originate
FreeSWITCHSherpa ASR (流式)WebSocket 10096/audio识别期audioFork.wsUrl、ASR wsPath否WS 握手、partial/final
FreeSWITCHFunASR ASR (2-pass 流式)WebSocket 10099/audio识别期audioFork.wsUrl、ASR wsPath否WS 握手、partial/final
callout-server / 业务方FunASR ASR (离线)HTTP 10094/asr否asrPath、/v1/audio/transcriptions否POST /asr 转写
callout-server / 业务方Sherpa ASR (离线)HTTP 10095/asr否asrPath、/v1/audio/transcriptions否POST /asr 转写
callout-serverASRWebSocket 10096/audio 或 10099/audio回铃检测期ringbackDetection.wsUrl否回铃检测日志
callflow-eslTTSHTTP 9080请求或流式响应tts.profiles.*.endpoint否POST /tts、POST /tts-stream
FreeSWITCHTTSHTTP(S) 媒体 URL播放期TTS server.publicBaseUrl仅 FS 可达即可curl 返回 URL
callflow-servercallflow-eslHTTP 9912否callflowEsl.baseUrl/token否GET /health、runtime API
callout-servercallflow-eslHTTP 9912否cells[].callflowEsl.*否软电话会话代理
callflow-eslcallout-serverHTTP 9920否callout.baseUrl/token否进度 / 结果回写
callflow-eslcallout-server (NLU)HTTP 9920否callout.baseUrl/token否/health、/api/ai/nlu/capabilities
callflow-eslcallout-server (Chat)HTTP/SSE 9920单轮流式callout.baseUrl/token否capabilities、SSE complete
callout-server / callflow-esl 业务OpenAI 兼容推理后端HTTP /v1/chat/completions单轮流式businessConfig.*.endpoint、llm.openaiUrl 或 DB callout.ai_llm_configs否/v1/models、一次推理
callout-serverEmotionHTTP 9090/analyze否emotionAnalysis.endpoint否/health、POST /analyze
Bun 服务 / FreeSWITCHPostgreSQLTCP 5432连接池db.url、databases.*.url、DSN否pg_isready / SQL
callflow-esl / callout-serverRedisTCP 6379常驻redis.url否PING
浏览器Nginx 录音 / 情绪音频HTTPS 443否media.*BaseUrl、Nginx alias可只读开放HEAD / GET 文件

2. 部署场景 ​

2.1 通用主机名 ​

以下名称用于所有通用示例。内部 DNS 应返回私网地址,公网域名只用于浏览器、SIP/SDP、TURN 和确实需要跨边界访问的媒体。

名称角色
edge.example.com前端、API 与媒体 HTTPS 入口
sip.example.comWebRTC WSS、SIP realm 与 SIP/SDP 广告
turn.example.comTURN control 与 relay 广告
fs-a.internalFreeSWITCH cell A
callflow-esl-a.internalcell A 配对 callflow-esl
asr.internal / tts.internal / emotion.internal本地语音服务
openai.internalOpenAI 兼容推理后端
postgres.internal / redis.internal数据服务

2.2 单机部署 ​

所有进程位于同一主机时,内部 upstream 可使用 127.0.0.1。监听地址仍按访问者选择:只由 同机进程访问的服务绑定 127.0.0.1;需要接收外部 SIP、RTP、HTTPS 或 TURN 的组件绑定实际 网卡或 0.0.0.0,并用防火墙限制。

纯内网联调的关键地址:

toml
[esl-server]
host = "127.0.0.1"
port = 9911

[http-server]
host = "127.0.0.1"
port = 9912
authToken = ["<ESL_HTTP_TOKEN>"]

[freeswitch]
host = "127.0.0.1"
port = 8021
password = "<ESL_PASSWORD>"
callbackHost = "127.0.0.1"
callbackPort = 9911

[audioFork]
wsUrl = "ws://127.0.0.1:10096/audio"

[tts.profiles.wav]
endpoint = "http://127.0.0.1:9080/tts"

[tts.profiles.stream]
endpoint = "http://127.0.0.1:9080/tts-stream"
playbackPrefix = "audio_fork://"

TTS 同机时仍需区分控制地址与返回地址。若 FreeSWITCH 同机,TTS 可广告 http://127.0.0.1:9080;只要任一播放者跨主机,就改为其可达的主机地址或 HTTPS 域名。

公网 WebRTC 在上述基础上增加:

  • sip.example.com:443 终止 TLS 并代理到本机 Sofia WSS;
  • external_sip_ip 和 external_rtp_ip 广告公网 IP;
  • FreeSWITCH RTP 与 coturn relay 使用两个不重叠的 UDP 范围;
  • 两个管理后端启用相同 TURN realm、host 和 shared secret,浏览器只获得短期凭据;
  • 录音与情绪音频通过 Nginx 只读发布,目录不可执行、不可列目录。

内网联调不需要 TURN 时保持 turn.enabled=false。公网浏览器跨运营商网络时不能把 TURN 当作可选优化,必须完成真实 allocation 与呼叫验收。

2.3 分层多机部署 ​

推荐最小放行规则:

边界允许方向最小端口
公网 → 边缘入站80/443 TCP、SIP 现场端口、3478 TCP/UDP、FS RTP UDP 范围、TURN relay UDP 范围
边缘 → 应用出站callflow / callout API 端口;Web upstream 仅 Nginx 需要
边缘 → AI出站ASR WS、TTS HTTP、OpenAI 兼容推理后端(仅业务确实直连时)
应用 → 边缘出站callflow-esl HTTP、每 cell inbound ESL、只读录音 HTTPS
应用 → AI出站Emotion、OpenAI 兼容推理后端
边缘 / 应用 → 数据出站PostgreSQL、Redis
AI → 数据出站默认无;只有新增明确依赖时开放

主机防火墙规则应同时限制源网段或源主机。禁止用“整个内部网段到所有端口”代替连接矩阵。 数据库和 Redis 若支持 TLS,跨不可信网络分别使用 postgresql TLS 参数与 rediss://。

2.4 每服务独立部署 ​

完整内部地址表:

服务DNS 与端口调用者
FreeSWITCH Afs-a.internal:<ESL_PORT>callflow-esl A、callout-server
callflow-esl A outboundcallflow-esl-a.internal:9911FreeSWITCH A
callflow-esl A HTTPcallflow-esl-a.internal:9912两个 API
callflow-servercallflow-api.internal:9913Nginx
callout-server (含 AI 策略中台)callout-api.internal:9920Nginx、callflow-esl
ASRasr.internal:10096FreeSWITCH、callout-server
TTStts.internal:9080callflow-esl、FreeSWITCH
Emotionemotion.internal:9090callout-server
OpenAI 兼容推理后端openai.internal:8000callout-server、callflow-esl
PostgreSQLpostgres.internal:5432FreeSWITCH 与各自研后端
Redisredis.internal:6379callflow-esl、callout-server

调用方配置替换表:

调用方字段目标示例
FreeSWITCH Adialplan socketcallflow-esl-a.internal:9911
callflow-esl Afreeswitch.host/portfs-a.internal:<ESL_PORT>
callflow-esl Afreeswitch.callbackHost/Portcallflow-esl-a.internal:9911
callflow-esl AaudioFork.wsUrlws://asr.internal:10096/audio
callflow-esl Atts.profiles.*.endpointhttp://tts.internal:9080/...
callflow-esl AbusinessConfig.*.endpointOpenAI 兼容后端 /v1/chat/completions 或 callout-server /api/ai/chat/completions
callflow-esl Acallout.baseUrlhttp://callout-api.internal:9920
callflow-esl Adb.url / redis.url数据层 DNS
callflow-serverdatabases.*.urlPostgreSQL DNS
callflow-servercallflowEsl.baseUrlhttp://callflow-esl-a.internal:9912
callout-servercells[].freeswitch.*对应 cell FreeSWITCH
callout-servercells[].callflowEsl.baseUrl对应 cell callflow-esl
callout-serveremotionAnalysis.endpointhttp://emotion.internal:9090/analyze
callout-serverllm.openaiUrlhttp://openai.internal:8000/v1/chat/completions(可选,留空走 DB 配置)

跨机部署还必须满足:

  • ASR 和 TTS 流式请求的代理关闭响应缓冲,读超时覆盖最长通话轮次;
  • WebSocket 代理传递 Upgrade / Connection,并保留 ASR 子协议;
  • FreeSWITCH 录音目录不能假设与 callout-server 共用文件系统,应由 cell 所在主机发布只读 URL;
  • TTS publicBaseUrl 从 FreeSWITCH 主机测试,而不是只从 callflow-esl 测试;
  • 内部 DNS 设置合理 TTL,证书 SAN 覆盖实际域名;长连接不会因 DNS 变化自动迁移;
  • 避免链路中重复 NAT。出现 WebSocket 随机断开或大包丢失时检查 MTU、MSS clamp 和代理超时;
  • 健康检查只证明新连接可建立,部署切换前还要评估已有 ESL、WS 和流式媒体连接的排空。

2.5 多 FreeSWITCH cell 与高可用 ​

一个 cell 固定由一台 FreeSWITCH 和配对的 callflow-esl 服务构成。cellId 会写入业务 记录并决定事件、录音与并发归属,必须稳定、唯一,投产后不要重命名。

网络相关模板:

toml
[redis]
url = "rediss://<USER>:<PASSWORD>@redis.internal:6379/0"
keyPrefix = "callout"
lockTtlMs = 60000

[[cells]]
id = "cell-a"
maxConcurrency = 100

[cells.freeswitch]
host = "fs-a.internal"
port = 8021
password = "<CELL_A_ESL_PASSWORD>"
callbackHost = "callflow-esl-a.internal"
callbackPort = 9911
originateDialStringTemplate = "user/{{destinationNumber}}"
agentDialStringTemplate = "user/{{agentExtension}}"
commandTimeoutMs = 15000

[cells.callflowEsl]
baseUrl = "http://callflow-esl-a.internal:9912"
token = "<CELL_A_HTTP_TOKEN>"

[cells.media]
freeswitchRecordingBaseUrl = "https://media.example.com/recordings/cell-a"

[[cells]]
id = "cell-b"
maxConcurrency = 100

[cells.freeswitch]
host = "fs-b.internal"
port = 8021
password = "<CELL_B_ESL_PASSWORD>"
callbackHost = "callflow-esl-b.internal"
callbackPort = 9911

[cells.callflowEsl]
baseUrl = "http://callflow-esl-b.internal:9912"
token = "<CELL_B_HTTP_TOKEN>"

[cells.media]
freeswitchRecordingBaseUrl = "https://media.example.com/recordings/cell-b"

多个 callout-server 可共享 PostgreSQL 和 Redis,并置于 HTTP 负载均衡后。每 cell inbound ESL 事件监听由 Redis 租约协调,同一时刻只应有一个消费者;这提供消费者接管能力,但不等于呼叫 无损自动迁移。至少监控租约续期、数据库连接、各 cell ESL 状态和可用并发。

以下边界需要连接归属或配对,不能直接套用普通无状态 HTTP 轮询:

  • FreeSWITCH outbound socket 必须进入本 cell 的 callflow-esl;在途呼叫不能迁移到另一实例;
  • inbound ESL 命令和事件必须命中承载目标 UUID 的 FreeSWITCH;
  • ASR WebSocket 属于当前媒体 bug 与识别轮次,断线后需要业务恢复,不会透明切换;
  • TTS /tts-stream 产生的资源和响应由创建它的 TTS 实例持有,负载均衡需粘性、共享资源存储, 或让返回 URL 精确回到原实例;
  • 录音 URL 必须按 attempt.cellId 选择 cell 对应的媒体源;
  • 扩容 callflow-esl 时,共享 Redis 只协调部分运行态,不会把既有 outbound ESL socket 搬迁。

2.6 可选语音后端 ​

主线配置使用本地 sherpa ASR/TTS。替换为云兼容网关时,只改变调用地址和 TTS 广告地址,不 改变 FreeSWITCH 到兼容接口的协议:

后端ASRTTS替换规则
本地 sherpa100969080audioFork.wsUrl 与两个 TTS endpoint 指向本地服务
阿里云网关100979081指向网关 /audio、/tts、/tts-stream,校准 tts.publicBaseUrl
Azure 网关100989082同上,使用 Azure 网关端口

网关 tts.publicBaseUrl 同样必须从 FreeSWITCH 可达。阿里云的 appKey/token 和 Azure 的 key、 region、endpoint 只能用相应环境变量或密钥系统注入,不能写入本文、仓库配置样例或前端构建 变量。详见阿里云网关与 Azure 网关。

3. 配置调整手册 ​

3.1 callflow-esl ​

下面只展示网络字段;合并时保留完整配置中的超时、业务和运行参数。

toml
[esl-server]
host = "0.0.0.0"
port = 9911

[http-server]
host = "0.0.0.0"
port = 9912
authToken = ["<ESL_HTTP_TOKEN>"]

[freeswitch]
host = "fs-a.internal"
port = 8021
password = "<ESL_PASSWORD>"
callbackHost = "callflow-esl-a.internal"
callbackPort = 9911

[audioFork]
wsUrl = "ws://asr.internal:10096/audio"

[tts.profiles.wav]
endpoint = "http://tts.internal:9080/tts"
playbackTarget = "wav-url"
fsPlaybackBaseDir = ""

[tts.profiles.stream]
endpoint = "http://tts.internal:9080/tts-stream"
playbackPrefix = "audio_fork://"

[businessConfig.llm-chat-business]
endpoint = "http://openai.internal:8000/v1/chat/completions"
apiKey = "<OPENAI_API_KEY>"
model = "<MODEL_NAME>"

[businessConfig.kb-audio-chat-business]
# 必须显式配置为 callout-server 的 POST /api/ai/chat/completions
endpoint = "http://callout-api.internal:9920/api/ai/chat/completions"
token = "<CHAT_TOKEN>"

[callout]
baseUrl = "http://callout-api.internal:9920"
token = "<CALLOUT_TOKEN>"

[db]
url = "postgres://<USER>:<PASSWORD>@postgres.internal:5432/freeswitch"

[redis]
url = "redis://<USER>:<PASSWORD>@redis.internal:6379/0"

[recording]
directory = "/srv/freeswitch/recordings"

逐字段按访问者判断:

字段实际访问者不能误填为
esl-server.host/portFreeSWITCH → callflow-eslFreeSWITCH 自己的回环地址
http-server.host/port两个 API / Nginx → callflow-esl只监听回环但调用者跨机
freeswitch.host/port/passwordcallflow-esl → FreeSWITCH inbound ESLoutbound ESL 9911
freeswitch.callbackHost/PortFreeSWITCH → callflow-esl outbound ESLcallflow-esl 所在机的 127.0.0.1(跨机时)
audioFork.wsUrlFreeSWITCH 的 mod_audio_fork → ASR仅 callflow-esl 可达的地址
tts.profiles.*.endpointcallflow-esl → TTSTTS 返回给 FreeSWITCH 的媒体基址
recording.directoryFreeSWITCH 写文件callflow-esl 本机但未共享的目录
db.url / redis.urlcallflow-esl → 数据层调用方跨机时的 localhost

audioFork.wsUrl 虽然位于 callflow-esl 配置中,真正建立 WebSocket 的是 FreeSWITCH。录音路径 也由 FreeSWITCH 写入;跨机时要使用 FreeSWITCH 本地路径或双方真实共享的挂载。

3.2 callflow-server 与前端 ​

toml
[server]
host = "0.0.0.0"
port = 9913

[databases.freeswitch]
url = "postgres://<RO_USER>:<PASSWORD>@postgres.internal:5432/freeswitch"

[databases.callflow]
url = "postgres://<USER>:<PASSWORD>@postgres.internal:5432/freeswitch?options=-c%20search_path=callflow"

[callflowEsl]
baseUrl = "http://callflow-esl-a.internal:9912"
token = "<ESL_HTTP_TOKEN>"

[turn]
enabled = true
host = "turn.example.com"
port = 3478
realm = "sip.example.com"
sharedSecret = "<INJECT_AT_RUNTIME>"
credentialTtlSeconds = 3600

callflow-server 当前没有 Redis 网络配置。前端 API 根地址在构建期注入:

bash
VITE_CALLFLOW_API_BASE_URL=/api bun run --cwd apps/callflow-webpage build
VITE_CALLOUT_API_BASE_URL=/api bun run --cwd apps/callout-webpage build

同域 /api 时,应为两个前端使用不同 origin,并分别代理到对应 API;同一 origin 的同一个 /api 不能同时指向多个后端。使用绝对 URL 时必须是浏览器可访问地址,并配置 HTTPS 与所需 CORS 策略。

3.3 callout-server ​

单 cell 可使用 cells 的单元素形式,避免从顶层兼容字段迁移到多 cell 时改变语义:

toml
[server]
host = "0.0.0.0"
port = 9920

[server.serviceApi]
authToken = "<CALLOUT_TOKEN>"

[db]
url = "postgres://<USER>:<PASSWORD>@postgres.internal:5432/freeswitch"

[redis]
url = "redis://<USER>:<PASSWORD>@redis.internal:6379/0"
keyPrefix = "callout"
lockTtlMs = 60000

[emotionAnalysis]
endpoint = "http://emotion.internal:9090/analyze"

[llm]
openaiUrl = "http://openai.internal:8000/v1/chat/completions"
apiKey = "<API_KEY>"

[llm.nlu]
model = "qwen3.5-2b"
timeoutMs = 30000
maxTokens = 300

[llm.chat]
model = "qwen3.5-2b"
timeoutMs = 30000

[llm.script]
model = "qwen3.6-27b-fp8"
timeoutMs = 60000
maxTokens = 3000

[media]
emotionAudioBaseUrl = "https://media.example.com/emotion-audio"

[turn]
enabled = true
host = "turn.example.com"
port = 3478
realm = "sip.example.com"
sharedSecret = "<INJECT_AT_RUNTIME>"

[[cells]]
id = "cell-a"
maxConcurrency = 100

[cells.freeswitch]
host = "fs-a.internal"
port = 8021
password = "<ESL_PASSWORD>"
callbackHost = "callflow-esl-a.internal"
callbackPort = 9911

[cells.callflowEsl]
baseUrl = "http://callflow-esl-a.internal:9912"
token = "<ESL_HTTP_TOKEN>"

[cells.media]
freeswitchRecordingBaseUrl = "https://media.example.com/recordings/cell-a"

ringbackDetection.wsUrl 也由 callout-server 主机直接连接 ASR。两个后端的 TURN host、port、 realm 和 shared secret 必须与 coturn 一致;shared secret 优先通过 CALLFLOW_SERVER_TURN_SHARED_SECRET 和 CALLOUT_TURN_SHARED_SECRET 注入。

3.4 ASR、TTS 与 Emotion ​

toml
[server]
host = "0.0.0.0"
wsPort = 10096
httpPort = 10095
healthPath = "/health"
wsPath = "/audio"
wsSubprotocol = "audio.drachtio.org"

ASR 的调用者是 FreeSWITCH 和可选的 callout 回铃检测器。跨机时监听地址需覆盖目标私网网卡, 只向这些源开放。

toml
[server]
listenHost = "0.0.0.0"
listenPort = 9080
publicBaseUrl = "https://media.example.com/tts"

TTS listenHost/listenPort 供 callflow-esl 发送控制请求;publicBaseUrl 出现在合成响应中, 由 FreeSWITCH 发起 GET。两者可以是不同地址。

toml
[server]
host = "0.0.0.0"
port = 9090

[audio]
processedDir = "/srv/ai-voice/emotion-audio"

Emotion 把处理后的音频写入 audio.processedDir 并返回短文件名。Nginx 必须把该目录映射到 callout-server.media.emotionAudioBaseUrl 对应路径;Emotion 本身没有公开基址字段。 NLU、Chat、Script 生成与仿真引擎已原生内嵌于 callout-server(见 3.3 节),直接读取其 [llm] 配置或数据库 callout.ai_llm_configs。

3.5 FreeSWITCH ​

inbound ESL ​

xml
<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <param name="listen-ip" value="<FS_PRIVATE_IP>"/>
    <param name="listen-port" value="8021"/>
    <param name="password" value="<ESL_PASSWORD>"/>
    <param name="apply-inbound-acl" value="callflow-esl-inbound"/>
  </settings>
</configuration>
xml
<list name="callflow-esl-inbound" default="deny">
  <node type="allow" cidr="<CALLFLOW_ESL_CIDR>"/>
  <node type="allow" cidr="<CALLOUT_SERVER_CIDR>"/>
</list>

<list name="domains" default="deny">
  <node type="allow" cidr="<NGINX_TO_SOFIA_CIDR>"/>
</list>

<list name="softphone-turn-relay" default="deny">
  <node type="allow" cidr="<TURN_PUBLIC_IP>/32"/>
</list>

密码不能替代网络 ACL;端口不得直接面向公网。配置中的 listen-port 必须与两个调用方的 freeswitch.port / cells[].freeswitch.port 一致。Sofia 使用的来源 ACL 必须允许实际 Nginx 上游地址,candidate ACL 必须与后端下发的 TURN 公网地址一致;不要放宽为任意公网来源。

outbound ESL 与模块 ​

xml
<extension name="callflow-esl-socket">
  <condition field="destination_number" expression="^<SERVICE_PREFIX>">
    <action application="socket" data="callflow-esl-a.internal:9911 async full"/>
  </condition>
</extension>
xml
<load module="mod_event_socket"/>
<load module="mod_audio_fork"/>
<load module="mod_shout"/>

mod_shout 仅在使用 shout:// MP3 流播放时必需;WAV 播放还应保留现场所需的 HTTP / 文件 模块。模块是否已加载可用 show modules 核对。

SIP、SDP 与 RTP ​

xml
<!-- vars.xml:绑定地址与广告地址分离 -->
<X-PRE-PROCESS cmd="set" data="domain=sip.example.com"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=<PUBLIC_IP>"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=<PUBLIC_IP>"/>
xml
<!-- Sofia profile -->
<param name="sip-ip" value="<FS_PRIVATE_IP>"/>
<param name="rtp-ip" value="<FS_PRIVATE_IP>"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
<param name="ws-binding" value=":5066"/>
<param name="wss-binding" value=":7443"/>
<param name="local-network-acl" value="none"/>
<param name="apply-candidate-acl" value="softphone-turn-relay"/>
xml
<!-- switch.conf.xml;范围按容量规划,且不与 TURN relay 重叠 -->
<param name="rtp-start-port" value="<FS_RTP_START>"/>
<param name="rtp-end-port" value="<FS_RTP_END>"/>

sip-ip / rtp-ip 必须是本机可绑定地址,external_* 是写入 SIP/SDP 的公网广告地址。 NAT 后的浏览器场景应同时检查 Sofia local-network-acl、candidate ACL 和公网防火墙,不能只 修改 external_rtp_ip。

录音、数据库与证书 ​

  • record-path、会议 auto-record 和 callflow-esl recording.directory 应落到同一套可写目录 约定;按 cell 从对应 FreeSWITCH 主机只读发布。
  • FreeSWITCH PostgreSQL DSN 的 host 必须从 FreeSWITCH 主机可达。不要把完整含密码 DSN 放进 公开文档、日志或命令历史。
  • WSS、SIP TLS 和 DTLS 证书必须属于目标域名且处于有效期,私钥仅允许运行用户读取。仓库内 参考证书不能用于生产。

生效边界 ​

变更最小操作备注
dialplan、directory、普通 XML 变量reloadxml只影响后续匹配;用新呼叫验证
ACL 内容reloadacl新连接 / 新 candidate 生效
Sofia profile 的绑定、WSS、NAT 参数重启对应 Sofia profile会影响注册与在途 SIP 会话,安排窗口
模块加载、event socket 核心监听、RTP 全局范围、底层库完整重启 FreeSWITCH先排空通话并准备回滚

3.6 Nginx ​

以下模板展示路径关系,证书路径和安全策略按现场管理:

nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream callflow_api { server callflow-api.internal:9913; }
upstream callout_api { server callout-api.internal:9920; }
upstream asr_ws { server asr.internal:10096; }
upstream tts_http { server tts.internal:9080; }

server {
    listen 443 ssl;
    server_name flow.example.com;

    location /api/ {
        proxy_pass http://callflow_api;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    location / {
        root /srv/www/callflow;
        try_files $uri $uri/ /index.html;
    }
}

server {
    listen 443 ssl;
    server_name sip.example.com;

    location / {
        proxy_pass https://<FS_PRIVATE_IP>:7443;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
    }
}

server {
    listen 443 ssl;
    server_name media.example.com;

    location /asr/ {
        proxy_pass http://asr_ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_buffering off;
    }
    location /tts/ {
        proxy_pass http://tts_http/;
        proxy_buffering off;
        proxy_read_timeout 120s;
    }
    location /recordings/cell-a/ {
        alias /srv/freeswitch/recordings/;
        limit_except GET { deny all; }
        autoindex off;
    }
    location /emotion-audio/ {
        alias /srv/ai-voice/emotion-audio/;
        limit_except GET { deny all; }
        autoindex off;
    }
}

ASR 并不需要默认公开给浏览器;只有确实跨边界且无法使用私网时才增加公网 WSS,并加入鉴权、 来源限制和容量保护。不要通过 Nginx 暴露 ESL、数据库、Redis 或 OpenAI 兼容推理后端。

3.7 coturn ​

ini
listening-ip=0.0.0.0
listening-port=3478
external-ip=<TURN_PUBLIC_IP>/<TURN_PRIVATE_IP>
min-port=<TURN_RELAY_START>
max-port=<TURN_RELAY_END>
realm=sip.example.com
use-auth-secret
static-auth-secret=<INJECT_IN_PRIVATE_CONFIG>
fingerprint
no-multicast-peers

公网/私网映射写法用于一对一 NAT;直接绑定公网 IP 时按 coturn 版本与网络模型调整。控制端口和 整个 relay 范围都需放行,relay 范围不得与 FreeSWITCH RTP 范围重叠。生产优先配置 TLS/TURNS; 若现场暂未启用,必须至少提供 TURN/UDP 与 TURN/TCP,并使用短期 HMAC 凭据。

3.8 FRP ​

FRP 只应用于无法建立私网路由的应用 / AI TCP upstream。FRPS 代理端口应绑定回环或专用内网, 再由同机 Nginx / 应用消费:

toml
bindPort = 7000
proxyBindAddr = "127.0.0.1"

auth.method = "token"
auth.token = "<INJECT_FROM_SECRET_STORE>"
toml
serverAddr = "<FRPS_PUBLIC_OR_PRIVATE_IP>"
serverPort = 7000
auth.method = "token"
auth.token = "<INJECT_FROM_SECRET_STORE>"

[[proxies]]
name = "asr"
type = "tcp"
localIP = "127.0.0.1"
localPort = 10096
remotePort = <LOOPBACK_UPSTREAM_PORT>

不得用 FRP 承载高带宽、低抖动敏感的 FreeSWITCH RTP 或 TURN relay;公网 SIP WSS 也应在 FreeSWITCH 所在边缘主机就近终止。FRP token 只存在于私有配置,先运行配置校验,再滚动重启。

4. 安全边界 ​

4.1 端口分区 ​

分区允许的典型入口
公网入口HTTPS/WSS、明确的 SIP 入口、TURN control、FreeSWITCH RTP、TURN relay
跨主机内部outbound/inbound ESL、应用 API、ASR/TTS/Emotion、数据库、Redis、OpenAI 兼容推理后端
仅回环同机服务间 HTTP、FRPS 代理落点、未启用远程管理的诊断端口

以下服务不得直接暴露公网:FreeSWITCH inbound ESL、callflow-esl outbound ESL、PostgreSQL、 Redis、OpenAI 兼容推理后端和内部 callflow-esl HTTP API。跨主机时使用安全组、主机防火墙、ACL、 应用 token 和必要的 TLS 形成多层限制。

还需满足:

  • 浏览器入口只用 HTTPS/WSS,HTTP 仅用于跳转或受控健康检查;
  • SIP/SDP 的 NAT 广告地址与实际公网映射一致;
  • TURN shared secret、ESL 密码、内部 token、数据库口令均由密钥系统或受限环境变量注入;
  • 录音和情绪音频只读发布,禁用目录索引、上传、脚本执行和任意路径穿越;
  • 公开 ASR/TTS 时配置限流、连接上限、超时和访问控制;
  • 日志不得记录 Authorization、内部 token、TURN shared secret 或完整数据库 URL。

5. 部署与验收 ​

5.1 部署前检查表 ​

  • 为每条连接填完源 IP、目标 DNS/IP、端口、协议、TLS、鉴权和负责人。
  • 从最终调用者主机验证 DNS 解析,不以运维电脑的解析结果代替。
  • 确认所有 127.0.0.1 都是有意的同机访问,所有 0.0.0.0 只出现在监听字段。
  • 检查 TTS、SIP/SDP、TURN、录音和情绪音频的公开地址。
  • 检查 FreeSWITCH RTP 与 TURN relay 范围不重叠,安全组和主机防火墙同时放行。
  • 校验证书域名、完整链、有效期和私钥权限。
  • 校准代理 WebSocket Upgrade、读写超时、缓冲和请求体限制。
  • 检查数据库 / Redis 备份、连接上限和跨机加密。
  • 确认配置片段中的所有 <PLACEHOLDER> 已在私有配置中替换,且仓库无真实凭据。
  • 记录生效命令、维护窗口和逐组件回滚版本。

各场景在通用清单之外还需完成以下检查:

场景上线前增量检查
单机纯内网确认所有回环地址确实在同一网络命名空间;TURN 保持关闭;无管理端口意外绑定公网网卡
单机公网 WebRTC核对 NAT 公网映射、WSS 证书、SIP/SDP 广告、RTP 与 TURN relay 两段防火墙规则
分层多机按层逐条审批源 → 目标规则;从边缘和应用层分别测试 AI / 数据层;确认没有反向开放无调用需求的端口
每服务独立核对内部 DNS、证书 SAN、代理超时、MTU、录音跨机发布和 TTS 回源;准备 DNS 变更时的长连接排空方案
当前双主机 + FRP确认 FRPS 代理只绑定回环、12015 不属于 FRP、WSS/RTP/TURN/录音均按附录直连
多 cell / API 高可用固定并登记每个 cellId;校验每 cell ESL、回调、录音 URL 和并发;验证 Redis 租约接管但不宣称在途呼叫迁移

5.2 逐层连通测试 ​

在每个实际调用方主机执行,而不是只在服务本机执行:

bash
getent hosts fs-a.internal asr.internal tts.internal postgres.internal redis.internal
nc -vz fs-a.internal 8021
nc -vz callflow-esl-a.internal 9911
curl -fsS http://asr.internal:10096/health
curl -fsS http://tts.internal:9080/health
curl -fsS http://emotion.internal:9090/health
curl -fsS http://callout-api.internal:9920/health
curl -fsS http://callflow-esl-a.internal:9912/health

健康接口通过后继续验证协议:

  1. 用模块自带客户端连接 ASR /audio,发送 16kHz 单声道 PCM16,确认 partial / final。
  2. POST /tts,从 FreeSWITCH 主机 GET 返回的 wavUrl;再验证 /tts-stream 和 MP3 Content-Type。
  3. 使用有效内部 token 调用 NLU / Chat capabilities,并完成一次 NLU 与 SSE Chat 请求。
  4. 用短期 TURN 凭据分别完成 UDP 和 TCP authenticated allocation,确认 relay 端口落入规划范围。
  5. 从浏览器完成 WSS SIP REGISTER,检查证书、realm、401 challenge 和成功注册。
  6. 从 callflow-esl / callout-server 主机验证 inbound ESL 认证和命令,从 FreeSWITCH 建立 outbound socket。
  7. 分别访问每个 cell 的录音 URL和情绪音频 URL,确认只读、Content-Type 正确且无目录列表。

5.3 真实呼叫验收 ​

每种部署场景至少完成一通内网呼叫和一通真实公网 / 5G WebRTC 呼叫:

  • SIP 注册成功,WSS 无反复重连;
  • ICE 选中预期 host 或 TURN relay candidate;
  • DTLS 完成,双方 RTP 收发计数增长,双向有声;
  • FreeSWITCH 建立正确 cell 的 outbound ESL;
  • ASR 持续产生 partial,停顿后产生 final;
  • NLU / Chat 返回,TTS WAV 与流式 MP3 均可播放;
  • 通话结束后录音可通过该 cell 的 URL 访问;
  • Emotion worker 能获取录音、提交分析、写回结果,处理音频 URL 可播放;
  • callout 进度、最终结果、事件兜底和数据库状态一致;
  • 结束后没有遗留 ASR WebSocket、TTS stream、ESL 会话或 TURN allocation。

6. 常见错误 ​

现象常见原因修正
FreeSWITCH 无法连接 outbound ESLdialplan 使用了 FreeSWITCH 自己的 127.0.0.1,而 callflow-esl 跨机改为 FreeSWITCH 可达的 callflow-esl 地址并开放 9911/TCP
originate 接通后无业务接管callbackHost 从 FreeSWITCH 不可达从 FreeSWITCH 主机测试该 host/port
ASR 健康但通话无识别只验证了 callflow-esl → ASR,实际连接者 FreeSWITCH 不可达从 FreeSWITCH 验证 WS 路径、子协议和防火墙
TTS 合成成功但无声TTS publicBaseUrl 只对 TTS 自己或 callflow-esl 可达从 FreeSWITCH GET 返回 URL,修正广告地址 / 代理
WSS 握手失败或很快断开Nginx 丢失 Upgrade / Connection,证书域名错误,超时过短修正 WebSocket 代理与证书,延长读超时
WSS 注册成功但无媒体RTP / TURN relay 未放行,SDP 广告私网 IP,ICE candidate 错误对照 SDP、candidate、DTLS 和双方 RTP 计数逐层检查
TURN control 可连接但 allocation 失败只开了 3478,relay 范围未开放或 secret / realm 不一致放行完整 relay 范围并校准两端配置
媒体偶发断流代理缓冲、MTU / MSS、NAT 空闲回收或读超时关闭流式缓冲,检查路径 MTU、keepalive 与超时
录音在一个 cell 可播、另一个 404所有记录错误共用同一个录音 base URL按 cellId 配置 cells[].media.freeswitchRecordingBaseUrl
情绪结果有短文件名但页面 404processedDir 与 Nginx alias / emotionAudioBaseUrl 不一致对齐物理目录、URL 前缀和权限
内部管理端口出现在公网扫描结果服务绑定全部网卡且安全组 / 防火墙缺少源限制,或 FRP 绑定公网改为私网 / 回环绑定并增加源 ACL
FRP 上游能访问但媒体质量差错把 RTP、TURN relay 或 SIP WSS 放进 FRP将实时媒体与边缘信令迁到公网边缘主机直连

附录 A:当前双主机生产拓扑 ​

本节记录仓库当前部署参考,不是新的通用默认值。公开 IP 和域名是现有网络标识;凭据仍全部 使用占位符。实际变更前以现场配置、生产部署与落地、 FRP 说明和 WebRTC ICE/TURN 指南交叉核对。

A.1 主机职责 ​

主机组件网络职责
aliyunFreeSWITCH(已有常驻实例)、callflow-esl、FRPS、coturn、Nginx、两个 Web 前端 SPA(callflow 与 callout)与文档站公网 HTTPS/WSS、SIP/RTP、TURN;通过回环 FRP 使用应用与 AI 上游;本地发布录音与 Web 前端
WSL / 计算主机 (/data/ai-voice)callflow-server、callout-server、ASR、TTS、Emotion、PostgreSQL、Redis、MySQL、MinIO、FRPC提供应用、AI 与数据存储服务,通过 FRPC 隧道反向映射至 aliyun 本地回环

A.2 FRP 与 ESL 映射 ​

FRPS proxyBindAddr="127.0.0.1",因此下表端口只供 aliyun 本机 Nginx 和 callflow-esl 使用:

aliyun 回环端口后端
12000MySQL
12001Redis
12002MinIO API
12003MinIO Console
12004PostgreSQL
12008TTS(WSL 29080)
12009Emotion(WSL 29090)
12010ASR(WSL 20096)
12011callflow API(WSL 29913)
12012callout API(WSL 29920,含原生 AI 策略中台)

12005、12006、12007、12013 当前未分配:本地推理后端不经 FRP 暴露,Web 前端由 aliyun 本机 Nginx 静态托管而非隧道回源。隧道清单以仓库内 deploy/wsl-docker/frpc/frpc.toml 为准, 使用说明见 WSL 计算集群部署。

12015/TCP 是 aliyun 上 FreeSWITCH inbound ESL,不属于 FRP。它应用 callflow-esl-inbound ACL,只允许同机回环和明确的 Debian 出口地址;不得按 12000-12012 的回环代理方式理解。

A.3 公网信令与媒体 ​

能力当前地址路径
callflow Web/APIhttps://callflow.wangijun.comNginx 本地静态托管 SPA;/api/ → 回环 12011,/api/auth → 12012(统一登录),/esl-api/ → 本机 9912
callout Web/API (含 AI 策略中心)https://callout.wangijun.comNginx 本地静态托管 SPA;/api/ → 回环 12012
平台文档站https://ai-voice.wangijun.comNginx 本地静态托管 ai-voice-docs 构建产物
浏览器 SIP WSSwss://fs.wangijun.comNginx TLS → 本机 FreeSWITCH 私网 7443/WSS
ASR WSSwss://asr.wangijun.com/audioNginx → 回环 FRP 12010,仅需跨边界的调用者使用
TTS 媒体https://tts.wangijun.comNginx → 回环 FRP 12008
情绪音频https://emotion.wangijun.com/emotion-audioNginx / 文件映射与 Emotion 输出目录对齐
FreeSWITCH 录音https://recordings.wangijun.comaliyun Nginx 本地只读 alias
FreeSWITCH RTP123.57.205.60:30000-30199/UDP公网直达 aliyun,不经 FRP
TURN control123.57.205.60:3478/TCP+UDP公网直达 coturn
TURN relay123.57.205.60:31000-31040/UDP公网直达 coturn,与 RTP 范围分离

vars.xml 当前用 172.21.38.245 绑定 SIP/RTP,用 123.57.205.60 作为 external_sip_ip / external_rtp_ip。Nginx 到 FreeSWITCH WSS 仍保持加密上游。FRP 明确不 承载 SIP WSS、FreeSWITCH RTP、TURN control、TURN relay 或录音文件。

A.4 当前拓扑验收重点 ​

  1. aliyun 上 12000-12013 仅监听 127.0.0.1,公网无法直连。
  2. 12015/TCP 的 ACL 只包含明确的 callflow-esl / callout 来源。
  3. WSS 注册的 SIP realm 为 fs.wangijun.com,上游仍为 7443/WSS。
  4. 5G 浏览器选择 123.57.205.60:310xx relay,DTLS 完成且 RTP 双向增长。
  5. FreeSWITCH 的 RTP 落在 30000-30199,TURN relay 落在 31000-31040。
  6. ASR、TTS、API 经 FRP 断开时故障可见,但不影响 SIP/RTP/TURN 的边缘端口归属。
  7. 录音从 aliyun 本地 alias 读取,TTS 与情绪媒体分别按各自公开 URL 回源。

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。