默认绑 127.0.0.1 是有道理的:改成 0.0.0.0 之前想清楚

2026-08-09

很多人跑通 speech-to-speech 的第一件事,是想让同事、想让另一台机器上的前端、想让办公室里那台树莓派也能连上来试试。于是搜到 --host 这个参数,顺手改成 0.0.0.0,服务起来了,能连了,事情就算完了。

这一步走得太快了。这个默认值不是随便定的,官方在参数的帮助文本里已经把话说得很直白。

一、先把那行 help 原文读完

src/speech_to_speech/arguments_classes/realtime_server_arguments.py 只有三个参数,--host 是其中之一,类型 str,默认值 "127.0.0.1",帮助文本原文是:

Host interface for the Realtime server. Default is 127.0.0.1. Pass 0.0.0.0 explicitly to expose the unauthenticated API on the network.

注意 unauthenticated 这个词——它不是我加的形容,是写在 --help 输出里的。同一个文件里 --port 默认 8765。README 侧的说法与之一致:serve 默认绑定 127.0.0.1,要暴露到网络必须显式传 --host 0.0.0.0;而 local 命令永远绑定环回,并把自带的麦克风/扬声器客户端连到 ws://127.0.0.1:<port>/v1/realtime

也就是说,官方在这里做了一个很明确的分工:local 是「只给这台机器上的你自己用」,压根没给你改的余地;serve 给了你改的余地,但把后果直接写进了 help。

再看 Realtime Engine 那份架构文档里的 Python 客户端示例,api_key 那一行写的是:

client = OpenAI(
    base_url="http://localhost:8765/v1",
    websocket_base_url="ws://localhost:8765/v1",
    api_key="not-needed",
)

not-needed —— 官方示例自己就承认服务端不校验这个字段。这三处线索指向同一件事:这个服务的访问控制不在服务里,在你的网络边界上。

二、改 host 之前,先摆清楚 8765 上到底有什么

绑到 0.0.0.0 之后暴露出去的不是「一个端口」,而是一组入口。按事实卡口径,与本篇直接相关的有这几条:

入口传输出现的前提
/v1/realtimeWebSocket默认存在
POST /v1/realtime/callsContent-Type: application/sdpWebRTC需要装 speech-to-speech[webrtc] extra
POST /v1/chat/completionsHTTP--enable_llm_proxy 且运行在 --llm_backend chat-completions
POST /v1/responsesHTTP--enable_llm_proxy 且运行在 --llm_backend responses-api

WebRTC 那条的握手是客户端 POST 一个 SDP offer,服务端返回 SDP answer(201,带 Location: /v1/realtime/calls/{call_id} 头),之后音频走 RTP 媒体轨、事件走 oai-events 数据通道。

后两条要单独说。README 关于 --enable_llm_proxy 的安全声明必须完整带出,不许弱化:服务端自己不做任何认证,也不做任何限流。 官方给的建议是只在受信网络上启用该代理,或者把服务部署在一个自己掌管访问控制的网关后面;README 举的例子是 s2s-endpoint 的计算副本充当这样的网关——只对「用 HF token 创建了会话的客户端」开放这些路径,按该 token 校验 API key,并按用户限流。

代理开启后的几条行为细节,正好说明为什么它比语音端点更敏感:请求是无状态的(每次发完整消息列表);代理到上游用的是服务端持有的 key,该 key 永不到达客户端;model 字段总是被覆写成服务端配置的 --model_name;服务端忽略客户端传的 API key。翻译成人话就是——谁能连上这个端口,谁就能用你的 key 打你配置的那个模型,而且你在服务端设的 --model_name 保证了他打的是哪个模型,却拦不住他打多少次。--llm_proxy_connect_timeout_s 默认 10.0,且官方注明读取没有超时(生成可能要几分钟),所以也别指望超时能替你兜底。

代理默认关闭,并且要求远端后端(chat-completionsresponses-api),否则返回 501 并附原因。默认关闭这一点值得庆幸,但它是一个你随手就能打开的开关。

顺带一提那个更容易被忽略的账单问题:默认配置下 LLM 那一环走的是 --llm_backend responses-api + --model_name gpt-5.4-mini,也就是打 OpenAI。所以就算你一个代理都没开,只要有人能连上 /v1/realtime 并说话,这一路请求同样是拿你的 $OPENAI_API_KEY 出去的。这是按默认配置的参数语义推出来的后果,不是实测账单。

三、方案一:别动 --host,把服务接到你面前来

大多数「想让别的机器连上来」的需求,其实是「想在另一台机器上调试」。这种情况下更省心的做法是保持默认绑定,用 SSH 端口转发把远端的环回口接到本地:

# 通用运维做法,非 speech-to-speech 官方文档内容
ssh -L 8765:127.0.0.1:8765 <user>@<your-server>

保持服务端一侧原样启动:

export OPENAI_API_KEY=...
speech-to-speech serve

然后在本地这台机器上,客户端连的仍然是官方 Quickstart 里的那个地址:

speech-to-speech talk --url ws://127.0.0.1:8765/v1/realtime

每个选项为什么在这:-L 8765:127.0.0.1:8765 把本地 8765 的流量隧道到远端的环回 8765,服务端的监听地址完全没变、依旧只接受环回连接;talk --url 后面必须是完整的 realtime url,路径 /v1/realtime 不能省。默认服务地址是 ws://localhost:8765/v1/realtime,端口跟着 --port 走,你改了 --port 这里也要跟着改。

端口转发是通用运维做法,不是该项目官方文档里的内容,它把「谁能访问」这个问题交给了 SSH 的账号体系,而不是交给一个不认证不限流的服务。

四、方案二:确实要暴露时,把暴露面压到最小

有些场景绕不开,比如你要给一台没有 SSH 通道的嵌入式设备提供服务。那就把这几件事一起做完再改 host:

speech-to-speech serve \
    --host 0.0.0.0 \
    --port 8765 \
    --llm_backend responses-api \
    --model_name "ggml-org/gemma-4-E4B-it-GGUF" \
    --responses_api_base_url "http://127.0.0.1:8080/v1" \
    --responses_api_api_key ""

逐条解释这里为什么这么写:

  • --host 0.0.0.0 是这次要做的事,同时也是把 unauthenticated API 放到网络上的那一步;
  • --port 8765 显式写出来,是为了后面验收时你知道该盯哪个端口,不用去猜默认值;
  • 后四行把 LLM 指向本机 llama.cpp(http://127.0.0.1:8080/v1--responses_api_api_key 传空字符串),出自 README 的「Fully Local」示例。这一步的意义在于:万一有人连上来对着你的服务说了一整天话,被消耗的是你自己那张卡的算力,而不是一个云端 API 的额度。
  • 注意这里没有 --enable_llm_proxy。它默认就是关的,只要你不主动开,/v1/chat/completions/v1/responses 这两条 HTTP 入口就不会出现。要暴露到网络就别顺手开代理,两件事同时做等于把「无认证的通用 LLM 端点」挂到网上。

以上为按官方参数语义组合的示例,未逐项实测,以官方文档与 --help 的实际输出为准。

关于访问控制本身,官方给的方向只有一句:只在受信网络上启用,或者放在一个你自己掌管访问控制的网关后面。至于具体用防火墙只放行某个网段、用 nginx 或别的网关做鉴权和限流——这些都是通用运维做法,不属于该项目的官方文档内容,需要你结合自己的环境评估。 这篇不会告诉你「这样配就安全了」,因为服务本身没有提供任何可以让人下这个结论的机制。

五、产出物长什么样:只说有依据的那部分

改完之后,能确定的变化其实很少,而且都可以事先说清楚:

  1. 监听地址--host 决定,端口由 --port 决定,默认 8765。这是一个 FastAPI / uvicorn 服务,同时暴露 WebSocket 与 WebRTC 两种传输。
  2. 并发上限是硬的--num_pipelines 默认 1,官方 help 写得很清楚——一个 uvicorn 服务监听 --port,把每个进来的客户端路由到下一个空闲的 pipeline,最大并发 websocket 会话数等于 num_pipelines,再多的连接会被拒绝。所以默认配置下第二个连接进不来。请把它理解成「资源池就这么大」,不要把它当限流或访问控制用——它挡不住一个人反复连、也不区分连的人是谁。
  3. local 不受影响:它永远绑环回,--host 在这条路径上不是给你留的口子。
  4. 代理没开就没有那两条 HTTP 入口;开了但后端不是远端后端,会拿到 501 并附原因。
  5. Docker 那条路要单独看:README 说 docker compose up 会启动一个跑 Gemma 4 的 llama.cpp 服务和实时服务,暴露端口 80808765。8080 是 llama.cpp,8765 是 Realtime——两个端口的归属别记反了,你在编排文件里做端口映射时放行的是哪一个,直接决定了外面能碰到什么。

我们没有部署过这个服务,上面每一条都是仓库源码与文档口径,不含任何抓包或连接测试的结论。

六、怎么验收:四步,别跳

第一步,确认监听地址真的变了(或真的没变)。 这是通用运维命令,不是项目自带的:

# Linux
ss -ltnp | grep 8765
# Windows PowerShell
netstat -ano | findstr :8765

你要看的只有一件事:绑的是 127.0.0.1 还是 0.0.0.0。这一步最容易出错的地方是改了 --port 却还在查 8765,或者上一次启动的进程没退干净,你查到的是旧进程。

第二步,确认 --enable_llm_proxy 的状态与你以为的一致。 最直接的办法是回头看你实际敲的那条启动命令有没有这个 flag,以及看 speech-to-speech serve -h 里它的默认值仍是 False。要提醒一句 README 里另一个很咬人的行为:CLI 只为选中的后端构造配置,未激活后端的已知选项仍然被接受,但会带警告地忽略。也就是说你把某个参数写错了位置、写到了没激活的后端上,程序不会报错停下,只会警告后照跑——所以「命令没报错」不等于「参数生效了」,得去 -h 里对默认值。

第三步,确认 LLM 那一环打的是谁。 如果你按方案二把 --responses_api_base_url 指向了本机 llama.cpp,就该确认 8080 上那个服务确实起着;如果你没指,默认的 responses-api 后端会去调远程服务。README 在讲离线运行时明确点了这一条:不加本地 base URL 覆盖,默认后端就是往外打的。

第四步,确认 --num_pipelines 与你的用途匹配。 默认 1 意味着只能有一个会话;多台设备要连,第二台会被拒绝。这不是 bug,是 help 里写明的行为。

七、什么情况说明你不该改这个默认值

  • 你只是想自己对着麦克风说话。local,它永远绑环回,这个问题根本不存在。
  • 你只是想在另一台机器上调试。 走第三节的端口转发,服务端监听地址不用动。
  • 你打算开着 --enable_llm_proxy 一起暴露。 官方对这个组合的措辞是受信网络或自管网关后面,没有第三种说法;你手上没有网关,就别开这个开关。
  • 你的 LLM 还指着云端 API。 先把 LLM 那一环换成自己掌控的服务再谈暴露,否则暴露的不只是一个语音接口。
  • 客户端和服务端之间隔着对称 NAT,或者服务跑在没暴露 UDP 的容器里。 这种情况下 WebRTC 直连本来就通不了,README 明确指出需要 TURN 服务器(ICE 配置走 SPEECH_TO_SPEECH_ICE_SERVERS 环境变量,不设则用 aiortc 默认的 host candidates 加 Google STUN)。这时你要解决的是媒体通路问题,不是把 host 改成 0.0.0.0 就能了事的。

这个仓库在 2026-08-09 的快照里有 11893 个 star,但 star 数说明不了任何关于安全性或稳定性的事。真正值得记住的只有 help 里那句话:默认是 127.0.0.1,改成 0.0.0.0 意味着你在网络上放了一个 unauthenticated 的 API。项目把选择权交给了你,也把它的边界一并交代清楚了——剩下的部分,得你自己在网络那一层补上。

延伸阅读


本文依据 speech-to-speech 官方仓库(github.com/huggingface/speech-to-speech)的 README、 src/speech_to_speech/arguments_classes/ 下的参数定义与 Realtime Engine 架构文档整理,核对日 2026-08-09。 本文内容为仓库源码与文档口径,我们没有安装、部署或调用过该服务,文中毫秒值均为参数默认值而非实测延迟。 参数与默认值随版本变动,请以 speech-to-speech serve -h 的实际输出为准。

安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。