有人把公网 HAProxy 主机指向内网应用,然后问浏览器为什么跟不上。代理通过自己的 VPN 到达应用,重定向返回一个 Location 头,随后浏览器尝试一个只存在于隧道内的地址。从外面看,页面就是卡住。决定权在于由哪一方打开第二条连接,而这正是重定向与后端的区别。

配置里看似相同的两条路径

重定向用一条响应结束这次交换。客户端读取 Location,并从它所在的位置发起全新请求,而那正是你从未掌控的网络。代理不再参与。之后的一切取决于客户端自己的 DNS、路由和信任,所以结果会因用户在哪里打开浏览器而不同:

GET /app        client -> proxy
302 Location    proxy  -> client   (http://10.40.0.20:8080/)
GET /app        client -> 10.40.0.20:8080   client network, no route

代理后端把第二段留在服务器一侧。代理沿已有的路径打开它,客户端始终只跟一个公网名字打交道。隧道一旦断开,客户端收到的是错误,而不是一个跟不上的 URL。这一差别也正是故障可诊断的原因,因为错误出现在有日志的地方:

GET /app        client -> proxy
GET /app        proxy  -> 10.40.0.20:8080   over the VPN
response        proxy  -> client

HAProxy 的重定向文档正好划出这条界线:用重定向答复客户端,与把请求转发给后端是两种不同的动作。两条指令在配置文件里只隔几行,行为却完全不同。一条结束请求,另一条开启由代理拥有的第二个请求。

浏览器需要、而服务器的 VPN 给不了的东西

浏览器自己的请求需要四样它可能没有的东西:私有名字的 DNS 答案、通往该地址的路由、访问监听服务的权限,以及对那项服务所出示证书的信任。代理主机凭自己的隧道具备全部四样。这些没有一样会转移给从未在该隧道里的客户端,因为隧道是主机的属性,不是 URL 的属性。

这个缺口解释了“几乎能用”的症状。第一个请求成功,地址栏变了,随后连接挂起或失败,而代理全程健康。代理配置没有坏,调整它也帮不了一个到不了 10.40.0.20 的客户端。要么客户端拿到路由,要么代理替它建立连接。

只有代理能到达应用时

去掉重定向动作,把这些请求发给指向私有地址的后端。前端保留公网名字,由后端决定请求实际去哪里。客户端的路径没有任何变化,因为代理不再描述目的地,而是自己成为目的地。对于刻意监听内部端口 8080 的应用,server 块很短:

backend private_app
    mode http
    option httpchk GET /health
    server app1 10.40.0.20:8080 check

把这个块读作关于代理路由表的陈述:10.40.0.20 必须能从代理主机自身到达,这里意味着走 VPN 接口而不是默认路由。check 关键字只报告那条路由做了什么,健康检查路径必须存在于应用上,否则每次探测都会把服务器标为下线。检查通过并不能证明客户端如何,也替代不了决定谁可以使用后端的策略。

这段片段不是完整的前端。保留监听器、认证、允许的主机和路径选择,以及决定谁才能访问应用的规则。后端协议也要对上,因为 HTTPS 后端需要证书校验和服务器身份配置,跳过那项校验就把私有连接变成了未经验证的连接。

宣布成功之前,两个方向都要确认返回路径。应用现在看到的是来自代理的连接,所以它的日志和任何客户端地址元数据描述的都是代理,除非你另行安排。决定身份和转发地址如何处理,并实际测试这个决定,而不是假设它。

应用自己的重定向仍然指向内部

转发第一个请求只是整合的一半。应用可能发出绝对重定向、写死的资源 URL,或限定在它内部所知名字上的 Cookie,这些都会把浏览器送回它到不了的地址。指向内部名字的 Location 头看起来和最初的问题一模一样,所以它常被误诊为代理故障。

设置应用文档中的外部 URL 及其代理选项,然后让 Host 头与虚拟主机和证书预期一致。限定在内部名字上的 Cookie 不会经公网名字回来,随之而来的登录循环就会被读成认证缺陷。先改外部 URL,用干净的浏览器配置文件复测,并预期半迁移的配置会产生“登录成功、会话立刻死掉”的结果。

只信任来自指定代理的转发元数据。任何客户端都能设置的头部绝不能成为授权值,否则代理就成了调用方冒用他人身份的手段。这条规则与重定向问题无关,并且在之后每次重新设计中都成立。

确定你实际拥有的是哪种设计

如果所有用户都已经有获授权的 VPN 访问,那么重定向到私有 URL 可以是有意为之的设计。明确确认这个前提,而不是假设它,因为两种设计服务的人群完全不同。其中一种在酒店网络里完全不能用,另一种则把应用暴露给任何能加载公网 URL 的人。这个区别应该写进设计说明。

在用户所在网络的机器上跑检查,并跟踪浏览器走过的每一跳,而不只是第一个响应。盯住地址栏和每一个后续请求,因为资源标签里一个绝对 URL 就足以打断已经通过第一跳的流程。登录必须能通过公网名字完成,应该被拒绝的资源必须继续被拒绝。

发布反向代理是独立的访问决定,不是把代理自己的 VPN 透明地扩展到任何能加载其公网 URL 的人。两者在图上看很像,区别在于谁被允许进入。等部署之后再决定这个问题,就是内部应用意外变成公网应用的方式。

写下你建的是哪一种、为什么。下一个接手配置的人会假设是另一种。为图方便加的一个重定向,就足以重建最初的故障,症状会和你刚修好的那个一模一样。

相关文章