只在笔记本上响应、其他地方都不响应的开发服务器,通常绑定到了错误的地址。能接受连接却拒绝页面的服务器,通常是主机名校验失败。这是两类不同的故障,修法也不同,选对配置取决于谁需要访问工作区以及他们如何接入。

有三个问题决定答案:谁发起连接、能到达哪个地址,以及热重载是否必须对他们可用。

从套接字入手,而不是配置文件

在与启动它的相同网络上下文中,查看正在运行的进程实际绑定了什么,因为容器或命名空间可能持有一个宿主 shell 看不到的套接字。监听这一列回答第一个问题:只监听本地路径、某个具体接口,还是所有接口。

ss -ltn '( sport = :8080 )'
ss -ltnp | grep node

环回地址是安全的默认值,但对远程客户端毫无用处。通配符绑定很方便,会在任何有路由的接口上开放端口,这比大多数人在同时承载隧道的机器上想要的更宽。具体地址是折中选项,只有当接口在创建套接字那一刻确实拥有该地址时才有效。这个区别很重要,因为这三种绑定中有两种在浏览器里看起来一样,一旦出现第二个接口行为就完全不同。

在动手改路由或防火墙规则之前,先与这个观察对齐。如果进程监听在错误的地址上,任何策略路由都无法让客户端到达它。

按谁需要到达来选择绑定地址

如果只有这台机器上的你需要页面,就保留环回绑定并停下。如果同一隧道上的一位同事需要,就绑定隧道接口的地址,并让防火墙只允许那个来源。把开发服务器发布给正在积极测试它的人之外是一种最终会把工作区暴露到互联网的习惯。

如果容器内的浏览器需要访问服务器,绑定地址必须是容器网络能够路由到的地址,而这往往不是宿主为自己解析的地址。先确定属于哪种情况再改配置,因为三种情况会给出三个不同的值,只有一个是正确的。在这里靠猜,比配置的其他任何环节都更浪费时间。

然后按启动器真正读取的形式设置地址。配置文件和以编程方式构建服务器的脚本并不使用同一套选项:

devServer: {
  host: "10.77.0.20",
  port: 8080,
  allowedHosts: ["dev.corp.example"],
}

该片段中的地址只是示例,代表宿主拥有的 VPN 地址。如果项目用 new WebpackDevServer(options, compiler) 构建服务器,则应改为通过构造函数参数传入选项,因为那条路径不会读取 webpack 配置里单独的 devServer 属性。配置文件与 Node API 之间的这个差异,会让把片段复制进从不读取它的项目的人踩坑。dev-server 文档区分了两种启动方式,是核对所用版本的参考。

让主机名校验与你批准的名称一致

主机名校验的存在,是为了阻止从攻击者控制的名字提供的页面通过浏览器与你的开发服务器通信。用 allowedHosts: "all" 关掉它就移除了这层保护,而在能够访问内部服务的机器上,这点便利很少值得。一旦知道浏览器会发送哪些名字,正确满足校验并不昂贵。

列出你会确切使用的名字,包括解析到隧道地址的私有名称。如果客户端把某个名字解析到与你绑定的地址不同的地址,那是名称解析问题,不是服务器配置问题,把名字加进允许列表并不能修好它。要从客户端确认解析结果,而不是从服务器。

把三种控制区分清楚:绑定地址决定哪些接口接受连接,允许列表决定接受哪些 Host 头,认证决定谁被放行。只有第三项是访问控制,而开发服务器通常一项都没有。隧道地址也不是认证机制,因为能路由到它的人,早已在你所依赖的边界之内。

同时验证页面与热重载套接字

从目标客户端用配置好的协议发出一个新请求,读结果而不是读缓存。对 HTTPS 要保留预期的主机名和证书校验,因为浏览器警告不是一次成功的验收测试。页面能渲染、热重载却静默失败,是这里最常见的部分成功,快速检查时很容易漏掉。

curl -sS -o /dev/null -w "%{http_code} %{remote_ip}\n" http://dev.corp.example:8080/
curl -sS -i http://10.77.0.20:8080/ | head -n 5

然后打开浏览器的网络诊断,观察 WebSocket 连接。页面可能通过一个主机名、端口和协议加载,而重载套接字用的是另一个,所以两者必须分开检查。只有在部署路径确实需要不同值时,才配置文档中的客户端 URL,并且优先修路径,而不是教客户端绕行。

把监听地址、被接受的名字、允许的来源以及两项结果都记进项目笔记。这份记录会留给下一个启动工作区、并纳闷页面为什么能加载却从不刷新的人。

相关文章