Cloudflare OHTTP Gateway 封闭测试:拆分 Relay 与 Gateway,理清隐私请求链路

2026-10-02 31 预计阅读时间: 1 分钟
来源: blog.cloudflare.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

Cloudflare 宣布推出自助式 Cloudflare OHTTP Gateway 的封闭测试,同时将原来的 Privacy Gateway 更名为 Cloudflare OHTTP Relay。这个变化不只是换了产品名,而是更明确地把 Oblivious HTTP(OHTTP)链路中的两个角色拆开:Relay 负责接收客户端连接,Gateway 负责解封装并把请求送往目标服务。

对开发者而言,真正值得关注的是信任边界。普通 HTTPS 保护传输内容,但服务端仍能同时看到客户端网络身份和请求内容;OHTTP 则试图把这两类信息交给不同的基础设施处理。

Relay 和 Gateway 分别知道什么

一个典型的 OHTTP 请求可以抽象为:

Client
  │  封装并加密 HTTP 请求
  ▼
OHTTP Relay
  │  转发不透明消息
  ▼
OHTTP Gateway
  │  解封装请求
  ▼
Target Service

各角色的可见范围通常可以这样理解:

角色 可以看到 不应直接看到
Relay 客户端 IP、连接时间、加密消息大小、Gateway 地址 明文 HTTP 方法、路径、请求体
Gateway 解封装后的请求、目标服务、业务响应 客户端原始 IP
Target Service 来自 Gateway 的业务请求 客户端与 Relay 之间的直接连接信息

因此,更名后的 Cloudflare OHTTP Relay 对应前一跳转发角色;新公布的 Cloudflare OHTTP Gateway 则承担解封装和访问目标服务的角色。自助式 Gateway 进入封闭测试,意味着接入方有机会更直接地配置这部分隐私基础设施,但实际 API、密钥格式和准入流程仍应以测试文档为准。

这种设计并不等于“完全匿名”。消息长度、时间相关性、流量模式、应用层标识符,以及 Relay 与 Gateway 是否独立运营,都会影响最终隐私强度。如果解封装后的请求仍携带账号 ID、广告标识符或稳定设备指纹,网络层隔离也无法消除业务层关联。

接入时最容易混淆的三个边界

1. OHTTP 不是普通反向代理

普通反向代理通常能够读取 HTTP 内容,并通过 X-Forwarded-For 等字段保留客户端地址。OHTTP Relay 的目标恰好相反:它转发的是封装后的消息,不应理解业务请求,也不应把客户端 IP 注入内层请求。

接入时应检查目标服务是否错误依赖以下字段:

X-Forwarded-For
Forwarded
CF-Connecting-IP
True-Client-IP

如果限流、风控或地域判断完全建立在终端 IP 上,引入 OHTTP 后就需要重新设计。可以考虑短期令牌、匿名配额凭证、会话级速率限制,或者由可信组件签发的不可链接证明,而不是试图恢复原始 IP。

2. Gateway 是一个新的高价值边界

Gateway 能看到解封装后的请求,因此需要像保护 API 入口一样保护它:

  • 限定可访问的目标域名和端口,避免成为开放代理;
  • 禁止访问环回地址、链路本地地址和云元数据地址;
  • 对密钥进行轮换,并为旧配置保留有限的重叠时间;
  • 日志中避免记录完整请求体、认证令牌和稳定用户标识;
  • 为请求大小、超时、并发数和响应大小设置上限;
  • 将 Gateway 到目标服务的认证与最终用户身份分开。

3. 目标服务仍需验证业务权限

OHTTP 隐藏网络层关联,不会自动完成用户认证和授权。目标 API 仍然要验证访问令牌、签名、作用域和重放保护。更稳妥的模式是使用短生命周期、窄权限令牌,并避免令牌本身成为跨请求追踪标识。

用本地脚本理解三段式请求路径

下面的 Python 程序会在本机启动 Target、Gateway 和 Relay 三个 HTTP 服务,然后由客户端通过 Relay 发出一次请求。它可以直接运行,用于观察调用顺序和各层日志。

重要说明:这个示例仅演示拓扑和职责划分。为了保持零依赖,它使用 Base64 模拟“封装”,不提供任何加密或隐私保护,也不是 OHTTP 协议实现。生产环境必须使用兼容 OHTTP 标准和 Cloudflare 测试接口的实现。

将代码保存为 ohttp_topology_demo.py:

import base64
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.request import Request, urlopen

TARGET_PORT = 9103
GATEWAY_PORT = 9102
RELAY_PORT = 9101


def post(url: str, body: bytes, content_type: str) -> bytes:
    request = Request(
        url,
        data=body,
        method="POST",
        headers={"Content-Type": content_type},
    )
    with urlopen(request, timeout=5) as response:
        return response.read()


class QuietHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        return

    def read_body(self) -> bytes:
        length = int(self.headers.get("Content-Length", "0"))
        return self.rfile.read(length)

    def reply(self, body: bytes, content_type="application/octet-stream"):
        self.send_response(200)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


class TargetHandler(QuietHandler):
    def do_POST(self):
        body = self.read_body()
        payload = json.loads(body)
        print(f"[target] business path={payload['path']!r}")
        result = json.dumps(
            {"ok": True, "message": "processed by target"}
        ).encode()
        self.reply(result, "application/json")


class GatewayHandler(QuietHandler):
    def do_POST(self):
        envelope = self.read_body()
        # Demo only: Base64 is encoding, not encryption.
        inner_request = base64.b64decode(envelope)
        payload = json.loads(inner_request)
        print(f"[gateway] decoded request for {payload['path']!r}")

        target_response = post(
            f"http://127.0.0.1:{TARGET_PORT}/execute",
            inner_request,
            "application/json",
        )
        wrapped_response = base64.b64encode(target_response)
        self.reply(wrapped_response)


class RelayHandler(QuietHandler):
    def do_POST(self):
        opaque_message = self.read_body()
        print(
            "[relay] client=%s, opaque_bytes=%d"
            % (self.client_address[0], len(opaque_message))
        )
        gateway_response = post(
            f"http://127.0.0.1:{GATEWAY_PORT}/ohttp",
            opaque_message,
            "message/ohttp-req",
        )
        self.reply(gateway_response, "message/ohttp-res")


def start_server(port, handler):
    server = ThreadingHTTPServer(("127.0.0.1", port), handler)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    return server


def main():
    servers = [
        start_server(TARGET_PORT, TargetHandler),
        start_server(GATEWAY_PORT, GatewayHandler),
        start_server(RELAY_PORT, RelayHandler),
    ]
    time.sleep(0.2)

    inner_request = json.dumps(
        {"path": "/v1/private-operation", "value": 42}
    ).encode()
    envelope = base64.b64encode(inner_request)

    wrapped_response = post(
        f"http://127.0.0.1:{RELAY_PORT}/relay",
        envelope,
        "message/ohttp-req",
    )
    response = base64.b64decode(wrapped_response)
    print("[client] response:", response.decode())

    for server in servers:
        server.shutdown()


if __name__ == "__main__":
    main()

运行:

python3 ohttp_topology_demo.py

预期会看到类似输出:

[relay] client=127.0.0.1, opaque_bytes=68
[gateway] decoded request for '/v1/private-operation'
[target] business path='/v1/private-operation'
[client] response: {"ok": true, "message": "processed by target"}

把这个模型迁移到真实接入时,需要替换的不是简单的 Base64,而是完整的 OHTTP 封装、Gateway 公钥配置获取、响应解封装和密钥轮换逻辑。Relay 地址、Gateway 配置端点、媒体类型及访问凭证也需要以封闭测试提供的资料为准。

封闭测试阶段的接入清单

在申请或评估 Cloudflare OHTTP Gateway 时,可以先回答以下问题:

  • 哪些请求确实需要拆分客户端 IP 与业务内容?
  • Relay 和 Gateway 是否处于符合要求的独立信任边界?
  • Gateway 允许访问哪些目标,是否默认拒绝未知目的地?
  • 客户端如何发现并缓存 Gateway 公钥配置?
  • 密钥轮换失败时,客户端采用失败关闭还是回退到普通 HTTPS?
  • Relay、Gateway 和目标服务分别记录哪些日志,保留多久?
  • 限流和反滥用机制是否会重新引入稳定身份标识?
  • 超时、重试和消息填充策略是否可能泄露请求类型?
  • 客户端不支持 OHTTP 时,产品是否允许降级,降级是否明确告知用户?

OHTTP 最适合那些“不希望单一中间方同时掌握来源和内容”的请求,例如遥测提交、隐私敏感查询或匿名令牌兑换。它会增加密钥管理、额外网络跳数、故障排查和反滥用设计的复杂度,因此不必机械地覆盖所有 API。

Cloudflare 这次产品拆分提供了更清晰的语言:Relay 处理来源侧连接,Gateway 处理内容侧请求。真正的落地重点不是名称,而是能否让这两个角色保持最小信息可见、最小日志记录和明确的失败策略。


相关推荐