Skip to main content
Proxy 让服务可以把自己的 HTTP 服务器放在内核背后,而不必让每个请求都经过服务协议处理。内核会把匹配的 HTTP 请求和 WebSocket 升级请求,直接转发到该服务拥有的一个地址上。当服务已经运行着自己的、有能力处理请求的服务器时——比如内置的 Web 框架、预先构建好的前端开发服务器,或者带有自身协议需求的长期运行进程——如果要通过请求/响应式的 HTTP 传输重新实现这些行为会很浪费甚至不可行,这时就适合使用 proxy。

注册一个 proxy 传输

和其他传输一样,proxy 传输有一个 id 和一个值为 proxytype,此外还有一个嵌套的 proxy 块,用于指定上游目标:

网络类型

  • tcp —— 上游监听在某个主机和端口上,address 就是对应的 host:port。适用于本身已经绑定了 TCP 端口的上游。
  • unix —— 上游监听在一个 Unix 域套接字上,address 是该套接字的文件系统路径。这样可以避免为只需要到达内核的流量暴露一个 TCP 端口。
  • inherited —— 仅适用于 grpc 服务。在内核启动该服务的进程之前,它会先准备好一个监听套接字,并以已打开的状态交给该服务,这样服务自己的服务器就可以立即开始接受连接,而不必和内核竞争绑定某个端口,也不需要协调端口号。一个继承的监听器可以承载该服务的任意数量的 proxy 路由。
unixtcp 目标指向的是已经在别处监听的地址,因此它们也可以在 static 服务的 manifest.yaml 中声明——参阅 Static 服务inherited 目标只对内核正在主动启动的 grpc 服务才有意义。

为什么对 grpc 服务而言 inherited 是更安全的默认选择

对于 grpc 服务,相比让服务自己绑定一个 unixtcp 监听器、再让某个传输指向它,更推荐使用 inherited。因为内核会在服务进程存在之前就自己创建好监听器,然后才把它以已打开的状态交给该进程,服务自行绑定监听器所带来的几种风险都不复存在:
  • 没有任何东西能抢先于服务绑定。 使用 unixtcp 目标时,服务必须在自己启动之后再自行绑定该地址,这就留下了一个窗口期,另一个本地进程有可能抢先绑定同一个路径或端口,从而拦截本应发给该服务的流量。使用 inherited 时,内核在服务进程甚至还没启动之前就已经持有该监听器,因此不存在其他进程可以抢占的窗口期。
  • 从一开始就是收紧的权限。 内核准备的监听器所在的目录权限为 0700,监听器本身权限为 0600。在把它交出去之前,只有内核自己的进程用户能够访问它,因此它不会像服务自行创建的套接字那样,因为初始化代码使用了较宽松的默认权限,而短暂地对同组用户或所有人可见。
  • 服务永远不需要绑定权限。 服务接收到的是一个已经打开的监听器,而不是自己创建一个,因此它不需要绑定端口或创建套接字文件的权限。这在使用 RunAsUser 让服务以受限的操作系统用户身份运行时尤其重要——该用户不需要任何特殊的网络权限,即使服务被攻破,也无法借此打开其他非预期的监听器。
  • 保证到达正确的进程。 因为内核创建了该监听器,并直接把它交给了自己刚启动的那个特定子进程,所以到达该监听器的流量必然会到达那个确切的服务实例。这里不存在需要查找的地址,也不可能连接到某个碰巧抢先绑定了同一路径的冒充进程。
  • 自动清理。 服务启动后,内核会关闭自己持有的引用;服务卸载时,内核也会移除该监听器,因此一个已停止的服务不会留下存活或残留的端点供其他进程发现或复用。
对于 unixtcp,同等程度的信任需要靠您自己的部署方式来保证:将 unix 套接字路径放在只有目标用户才能写入的目录中,并将 tcp 目标绑定在 localhost 或私有网络上,而不是可公开访问的接口上。这些做法仍然是合理的选择——对于 static 服务固定的上游而言,它们也是唯一的选择——但对于内核自己启动的 grpc 服务来说,inherited 省去了把这个部署细节做对的负担。

绑定一条路由

绑定到 proxy 传输的路由,携带的是与其他任何 HTTP 路由相同的声明——patternaccess,具体说明见路由与传输。proxy 路由通常使用以斜杠结尾的模式覆盖整个子树,让该前缀下的所有路径都能到达上游。对于 proxy 路由而言,根模式 / 始终被当作子树处理,可以匹配所有绝对路径,因此如果服务拥有根路径,就可以用一条路由代理整个应用。

内核保留和注入的内容

该代理会转发端到端的请求头,包括 CookieAuthorization,并保留原始的请求路径、查询字符串和 Host 请求头。逐跳(hop-by-hop)请求头按反向代理的常规方式处理。 在转发之前,内核总是会移除调用方提供的任何身份标识请求头,并注入自己经过验证的版本:
  • X-Arupa-Authenticated
  • X-Arupa-User
  • 用户所属的每个组各对应一个 X-Arupa-Group 请求头
这意味着上游可以信任这些请求头就是内核自身的验证结果——调用方无法通过发送自己伪造的版本来仿冒它们。

访问控制

访问权限的评估方式与任何 HTTP 路由相同:先检查服务级策略,再检查路由自己的 access 策略。关于策略模型,请参阅访问控制。内核会在代理请求之前检查访问权限,因此被拒绝的请求永远不会到达上游。

冲突

proxy 路由遵循路由与传输中描述的常规 HTTP 路由冲突规则:同一个路径不能被两个服务拥有,一个接受任意方法的路由会与同一路径下所有指定了具体方法的路由发生冲突。proxy 路由不是 static 的,因此不会触发 static 与非 static 的冲突规则——它可以和一个 HTTP 传输的路由共享同一片路径空间,就像两条被动态处理的路由那样,仍然遵循常规的方法规则。

何时使用

当响应可以很自然地放进一次请求/响应消息、且您的服务愿意通过生成的 SDK 来处理它们时,优先使用 HTTP 传输。当服务已经拥有自己的服务器、需要持久的 WebSocket 或流式连接,或者把每个请求都通过服务协议转译一遍的代价过高时,请选用 proxy。