Compare commits

...

2 Commits

Author SHA1 Message Date
youfak
c267f77eee feat: distribute credentials in worker snapshots
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
2026-07-31 16:46:47 +08:00
youfak
013defbd35 feat: assemble gateway process runtime 2026-07-31 16:34:01 +08:00
27 changed files with 1631 additions and 222 deletions

View File

@ -20,7 +20,8 @@ Proxy Pool 用 Controller 协调这些变化,并让 Gateway 数据面只消费
## 使用方式 ## 使用方式
- **Gateway**:调用方连接平台,由平台选择上游代理并转发 HTTP 或 HTTPS - **Gateway**:调用方连接平台,由平台选择上游代理并转发 HTTP 或 HTTPS
CONNECT。当前已有传输、调度和保护链组件命令进程与控制面快照客户端待装配。 CONNECT。`proxy-gateway` 已装配本地监听、指标探针和控制面 Register/Watch/ACK/
Runtime 会话;带凭据 Proxy 分发仍待闭环。
- **Distribution**:调用方按条件提取真实代理;成功提取即独占消费,不支持归还、 - **Distribution**:调用方按条件提取真实代理;成功提取即独占消费,不支持归还、
续租或状态查询。 续租或状态查询。
- **Admin**:运维人员查询状态、启停 Upstream、切换 Routing并触发严格配置 - **Admin**:运维人员查询状态、启停 Upstream、切换 Routing并触发严格配置
@ -43,11 +44,12 @@ Proxy Pool 用 Controller 协调这些变化,并让 Gateway 数据面只消费
- **Gateway 组件**HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、重试、超时、 - **Gateway 组件**HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、重试、超时、
目的地址保护、本地快照存储、容量调度和 Worker 控制面 Register/Watch/ACK/ 目的地址保护、本地快照存储、容量调度和 Worker 控制面 Register/Watch/ACK/
Runtime 会话组件已有实现与定向测试。`SessionSupervisor` 会为可恢复控制面中断执行 Runtime 会话组件已有实现与定向测试。`SessionSupervisor` 会为可恢复控制面中断执行
有界退避重连。Controller 可向 Worker 下发无凭据引用的已归属 Proxy 及 Gateway 有界退避重连。Controller 可向 Worker 下发已归属 Proxy、Gateway Routing 与按引用去重的
Routing 快照。Gateway 会将 Routing 与 Proxy 原子编译为同一内存 View并只按当前未过期 凭据材料快照。Gateway 会将 Routing、Proxy 与凭据原子编译为同一内存 View并只按当前未过期
View 匹配请求,并在内存中按 Sequential、Random、Round Robin、Weighted 或 Least View 匹配请求,并在内存中按 Sequential、Random、Round Robin、Weighted 或 Least
Connections 选择上游。无候选时支持 reject、受 `waitTimeout` 限制的本地容量等待, Connections 选择上游。无候选时支持 reject、受 `waitTimeout` 限制的本地容量等待,
以及仍经过目标地址策略的 direct`proxy-gateway` 命令与凭据分发仍待装配。 以及仍经过目标地址策略的 direct`proxy-gateway` 通过独立控制面拨号地址维护
Session并仅在持有未过期 Snapshot 时 Ready凭据材料只保留在当前节点内存 View。
- **安全边界**Gateway、Distribution 与 Admin 使用各自的认证语义,并支持 - **安全边界**Gateway、Distribution 与 Admin 使用各自的认证语义,并支持
CIDR、可信代理、严格请求解析和敏感信息最小化。 CIDR、可信代理、严格请求解析和敏感信息最小化。
@ -55,7 +57,7 @@ Proxy Pool 用 Controller 协调这些变化,并让 Gateway 数据面只消费
```mermaid ```mermaid
flowchart LR flowchart LR
Client[调用方] -->|HTTP / CONNECT| Gateway[Gateway<br/>进程待装配] Client[调用方] -->|HTTP / CONNECT| Gateway[proxy-gateway]
Client -->|独占提取| Distribution[Distribution] Client -->|独占提取| Distribution[Distribution]
Operator[运维人员] -->|管理操作| Admin[Admin] Operator[运维人员] -->|管理操作| Admin[Admin]
subgraph CP[proxy-controller 已运行] subgraph CP[proxy-controller 已运行]
@ -66,7 +68,7 @@ flowchart LR
Controller --> Redis[(Redis)] Controller --> Redis[(Redis)]
Controller --> PostgreSQL[(PostgreSQL)] Controller --> PostgreSQL[(PostgreSQL)]
Checker[Checker<br/>健康链待闭环] -. Observation .-> Controller Checker[Checker<br/>健康链待闭环] -. Observation .-> Controller
Controller -. Snapshot 链待闭环 .-> Gateway Controller -. gRPC Snapshot .-> Gateway
``` ```
- **PostgreSQL** 只保存管理面状态,不保存 Proxy 明细或逐次提取记录。 - **PostgreSQL** 只保存管理面状态,不保存 Proxy 明细或逐次提取记录。
@ -76,7 +78,7 @@ flowchart LR
## 当前完成度 ## 当前完成度
截至 **2026-07-31**,实施计划检查项为 **53 / 7471.6%**。详情见 截至 **2026-07-31**,实施计划检查项为 **55 / 7474.3%**。详情见
[实施计划](docs/development/implementation-plan.md)和 [实施计划](docs/development/implementation-plan.md)和
[交付完成度审计](docs/requirements/completion-audit.md)。 [交付完成度审计](docs/requirements/completion-audit.md)。
@ -84,9 +86,9 @@ flowchart LR
提取与限流、Controller 的 Admin/Distribution/Metrics 监听,以及 PostgreSQL 提取与限流、Controller 的 Admin/Distribution/Metrics 监听,以及 PostgreSQL
管理状态WorkerControlPlane 的 Register、Snapshot ACK、Runtime 心跳接收和 管理状态WorkerControlPlane 的 Register、Snapshot ACK、Runtime 心跳接收和
Redis 会话栅栏。 Redis 会话栅栏。
- **部分完成**Gateway 传输与调度组件、Snapshot 本地存储、Worker ownership - **部分完成**Gateway Outcome 上报、Checker 调度与健康状态链、Docker Compose/
与运行态领域组件、Docker Compose/Kubernetes 静态部署清单和 protobuf 契约。 Kubernetes 运行时 mTLS Overlay以及 protobuf 契约。
- **待完成**带凭据 Proxy 分发、Gateway 进程装配、Outcome 上报、Checker 调度与健康状态链, - **待完成**Outcome 上报、Checker 调度与健康状态链,
以及 loadgen 和代表性集群压测。 以及 loadgen 和代表性集群压测。
检查项数量不等于生产就绪度。静态部署清单与 protobuf descriptor 验证也不代表 检查项数量不等于生产就绪度。静态部署清单与 protobuf descriptor 验证也不代表
@ -137,7 +139,20 @@ go run ./cmd/proxy-controller -config CONFIG_FILE
改用宿主机可达的存储地址。 改用宿主机可达的存储地址。
本地配置中的 `.invalid` Provider URL 是故障演示占位,不会提供真实代理。 本地配置中的 `.invalid` Provider URL 是故障演示占位,不会提供真实代理。
当前仓库没有 Gateway、Checker 或 loadgen 命令,因此不提供对应启动命令。 Gateway 已提供启动命令;需要先启用 Controller `controlPlane` 并配置匹配的 mTLS
证书(回环 fixture 可使用明文),再提供独立的拨号地址和 Worker 身份:
```powershell
go run ./cmd/proxy-gateway -config CONFIG_FILE `
-control-plane CONTROLLER_HOST:8443 `
-cluster-id CLUSTER_ID -worker-id WORKER_ID `
-instance-id INSTANCE_ID -zone ZONE
```
以上参数也可通过 `PROXY_POOL_CONTROL_PLANE_ADDRESS`、`PROXY_POOL_CLUSTER_ID`、
`PROXY_POOL_WORKER_ID`、`PROXY_POOL_INSTANCE_ID` 与 `PROXY_POOL_ZONE` 提供。
Gateway 的 `/livez`、`/readyz`、`/metrics` 使用配置中的 `metrics.listen`;无有效
Snapshot 时 `/readyz` 返回 `503`。Checker 与 loadgen 命令尚未实现。
## 关键配置与入口 ## 关键配置与入口
@ -170,8 +185,8 @@ go run ./cmd/proxy-controller -config CONFIG_FILE
ownership 索引,以及 Gateway 快照客户端。 ownership 索引,以及 Gateway 快照客户端。
- **P0 - Checker 健康链**Checker 调度、实际探测、Observation reducer以及 - **P0 - Checker 健康链**Checker 调度、实际探测、Observation reducer以及
`FETCHED -> AVAILABLE / SUSPECT / UNHEALTHY` 状态链。 `FETCHED -> AVAILABLE / SUSPECT / UNHEALTHY` 状态链。
- **P1 - Gateway 与 Routing**`proxy-gateway` 命令、五种 Routing 策略、 - **P1 - Gateway 与 Routing**Gateway 进程、快照凭据分发、五种 Routing 策略与
`onUnavailable`、动态容量调整和 Drain 闭环 `onUnavailable` 已接入;动态容量调整和 Drain 闭环待完成
- **P1 - 可观测与部署**:低基数业务指标、完整 Compose/Kubernetes 进程拓扑, - **P1 - 可观测与部署**:低基数业务指标、完整 Compose/Kubernetes 进程拓扑,
以及故障转移和恢复演练。 以及故障转移和恢复演练。
- **P2 - 容量证明**`proxy-loadgen`、HTTP/CONNECT/Extract 分场景压测,以及 - **P2 - 容量证明**`proxy-loadgen`、HTTP/CONNECT/Extract 分场景压测,以及

View File

@ -64,6 +64,16 @@ message WorkerSnapshot {
bytes checksum = 5; bytes checksum = 5;
repeated RoutingRule routing = 6; repeated RoutingRule routing = 6;
repeated OwnedProxy proxies = 7; repeated OwnedProxy proxies = 7;
// Credential materials are protected by the WorkerControlPlane mTLS session
// and retained only in the Gateway's current in-memory snapshot view.
repeated SnapshotCredential credentials = 14;
}
message SnapshotCredential {
string secret_ref = 1;
string credential_version = 2;
string username = 3;
string password = 4;
} }
message SnapshotDelta { message SnapshotDelta {

97
cmd/proxy-gateway/main.go Normal file
View File

@ -0,0 +1,97 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"os/signal"
"strings"
"syscall"
"proxy-pool/internal/config"
"proxy-pool/internal/gateway/bootstrap"
)
const (
configEnvironment = "PROXY_POOL_CONFIG"
controlPlaneAddressEnvironment = "PROXY_POOL_CONTROL_PLANE_ADDRESS"
clusterIDEnvironment = "PROXY_POOL_CLUSTER_ID"
workerIDEnvironment = "PROXY_POOL_WORKER_ID"
instanceIDEnvironment = "PROXY_POOL_INSTANCE_ID"
zoneEnvironment = "PROXY_POOL_ZONE"
)
type environmentLookup func(string) string
type gatewayRun func(context.Context, bootstrap.Options) error
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
os.Exit(execute(ctx, os.Args[1:], os.Getenv, bootstrap.Run, os.Stderr))
}
func execute(
ctx context.Context,
args []string,
getenv environmentLookup,
run gatewayRun,
stderr io.Writer,
) int {
flags := flag.NewFlagSet("proxy-gateway", flag.ContinueOnError)
flags.SetOutput(stderr)
configPath := flags.String("config", "", "configuration file path")
controlPlaneAddress := flags.String("control-plane", "", "remote Controller control-plane address")
clusterID := flags.String("cluster-id", "", "cluster identifier")
workerID := flags.String("worker-id", "", "unique Worker identifier")
instanceID := flags.String("instance-id", "", "unique process instance identifier")
zone := flags.String("zone", "", "availability zone identifier")
if err := flags.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if flags.NArg() != 0 {
_, _ = fmt.Fprintln(stderr, "proxy-gateway: unexpected positional arguments")
return 2
}
if getenv != nil {
setIfEmpty(configPath, getenv(configEnvironment))
setIfEmpty(controlPlaneAddress, getenv(controlPlaneAddressEnvironment))
setIfEmpty(clusterID, getenv(clusterIDEnvironment))
setIfEmpty(workerID, getenv(workerIDEnvironment))
setIfEmpty(instanceID, getenv(instanceIDEnvironment))
setIfEmpty(zone, getenv(zoneEnvironment))
}
if ctx == nil || run == nil || !validValue(*configPath) || !validValue(*controlPlaneAddress) ||
!validValue(*clusterID) || !validValue(*workerID) || !validValue(*instanceID) || !validValue(*zone) {
_, _ = fmt.Fprintf(stderr,
"proxy-gateway: -config, -control-plane, -cluster-id, -worker-id, -instance-id and -zone are required; "+
"environment fallbacks: %s, %s, %s, %s, %s, %s\n",
configEnvironment, controlPlaneAddressEnvironment, clusterIDEnvironment, workerIDEnvironment, instanceIDEnvironment, zoneEnvironment,
)
return 2
}
err := run(ctx, bootstrap.Options{
ConfigPath: *configPath, Resolver: config.OSResolver{}, ControlPlaneAddress: *controlPlaneAddress,
ClusterID: *clusterID, WorkerID: *workerID, InstanceID: *instanceID, Zone: *zone,
})
if err == nil || (errors.Is(err, context.Canceled) && ctx.Err() != nil) {
return 0
}
_, _ = fmt.Fprintf(stderr, "proxy-gateway: %v\n", err)
return 1
}
func setIfEmpty(target *string, value string) {
if target != nil && *target == "" {
*target = value
}
}
func validValue(value string) bool {
return value != "" && strings.TrimSpace(value) == value
}

View File

@ -0,0 +1,66 @@
package main
import (
"context"
"io"
"strings"
"testing"
"proxy-pool/internal/gateway/bootstrap"
)
func TestExecutePassesExplicitGatewayIdentity(t *testing.T) {
t.Parallel()
var captured bootstrap.Options
status := execute(context.Background(), []string{
"-config", "gateway.yaml",
"-control-plane", "127.0.0.1:8443",
"-cluster-id", "cluster-a",
"-worker-id", "worker-a",
"-instance-id", "instance-a",
"-zone", "zone-a",
}, func(string) string { return "" }, func(_ context.Context, options bootstrap.Options) error {
captured = options
return nil
}, io.Discard)
if status != 0 {
t.Fatalf("execute() status = %d, want 0", status)
}
if captured.ConfigPath != "gateway.yaml" || captured.ControlPlaneAddress != "127.0.0.1:8443" ||
captured.ClusterID != "cluster-a" || captured.WorkerID != "worker-a" ||
captured.InstanceID != "instance-a" || captured.Zone != "zone-a" {
t.Fatalf("bootstrap options = %+v", captured)
}
}
func TestExecuteReadsGatewayOptionsFromEnvironment(t *testing.T) {
t.Parallel()
values := map[string]string{
configEnvironment: "gateway.yaml",
controlPlaneAddressEnvironment: "127.0.0.1:8443",
clusterIDEnvironment: "cluster-a",
workerIDEnvironment: "worker-a",
instanceIDEnvironment: "instance-a",
zoneEnvironment: "zone-a",
}
var captured bootstrap.Options
status := execute(context.Background(), nil, func(name string) string { return values[name] }, func(_ context.Context, options bootstrap.Options) error {
captured = options
return nil
}, io.Discard)
if status != 0 || captured.WorkerID != "worker-a" || captured.ControlPlaneAddress != "127.0.0.1:8443" {
t.Fatalf("execute() = (%d, %+v)", status, captured)
}
}
func TestExecuteRejectsIncompleteGatewayOptions(t *testing.T) {
t.Parallel()
var output strings.Builder
status := execute(context.Background(), []string{"-config", "gateway.yaml"}, func(string) string { return "" }, nil, &output)
if status != 2 || !strings.Contains(output.String(), controlPlaneAddressEnvironment) {
t.Fatalf("execute() = (%d, %q), want usage error naming missing environment", status, output.String())
}
}

View File

@ -235,9 +235,9 @@ Provider Parser 继续通过 `credentials.Store` 生成 `SecretRef` 和
在活动池状态提交之前。Distribution 所需凭据只保存在 Proxy 硬 TTL 和幂等 TTL 在活动池状态提交之前。Distribution 所需凭据只保存在 Proxy 硬 TTL 和幂等 TTL
约束内Extract 脚本可原子保存完整重放响应。 约束内Extract 脚本可原子保存完整重放响应。
Gateway Snapshot 继续只携带凭据引用Gateway 通过控制面下发到节点内存的 Gateway Snapshot 携带凭据引用及按引用去重的材料;材料只经 mTLS 控制面进入
凭据材料解析引用,不在请求热路径查询 Redis。凭据分发与轮换属于独立后续 Gateway 当前内存 View不在请求热路径查询 Redis也不进入 PostgreSQL。凭据轮换的
实现,不改变本 ADR 的活动池边界。 主动推送仍属于后续实现,不改变本 ADR 的活动池边界。
### 库存真值 ### 库存真值

View File

@ -27,14 +27,17 @@ Controller 已实现并验证 `RegisterWorker`、`AcknowledgeSnapshot` 和
`WatchSnapshots` 已在 Register 后发送与当前 ownership epoch 对应的基础完整快照, `WatchSnapshots` 已在 Register 后发送与当前 ownership epoch 对应的基础完整快照,
Gateway 校验后 ACK 并开始 Runtime 心跳。Controller 会从 Redis 的有界 Worker ownership Gateway 校验后 ACK 并开始 Runtime 心跳。Controller 会从 Redis 的有界 Worker ownership
索引构建无凭据引用的已归属 Proxy 内容,并将租约到期收紧到 Proxy 的 索引构建已归属 Proxy 内容,并将租约到期收紧到 Proxy 的 `usable_until`。Proxy 引用的
`usable_until`;带凭据 Proxy 会 fail-closed直到凭据材料分发完成。完整 Snapshot 凭据材料按 `secret_ref + credential_version` 去重,随完整 Snapshot 经 mTLS 下发,仅保留在
Gateway 当前内存 View。完整 Snapshot
已从配置原始顺序和 Admin 当前状态合成 Gateway Routing并与 Proxy 一起纳入 checksum 已从配置原始顺序和 Admin 当前状态合成 Gateway Routing并与 Proxy 一起纳入 checksum
Gateway 已将该 payload 编译并原子发布到与 Proxy 相同版本的本地 View动态 Router 只匹配 Gateway 已将该 payload 编译并原子发布到与 Proxy 相同版本的本地 View动态 Router 只匹配
当前未过期 View派发器已按五种策略从该 View 选择上游,且在 Proxy 容量耗尽时只在该 当前未过期 View派发器已按五种策略从该 View 选择上游,且在 Proxy 容量耗尽时只在该
View 的其余候选中回退。`wait_timeout` 随 `on_unavailable=WAIT` 下发并在 Gateway 作为有界 View 的其余候选中回退。`wait_timeout` 随 `on_unavailable=WAIT` 下发并在 Gateway 作为有界
本地容量等待使用;`DIRECT` 仍先经过 TargetPolicy 再建立 HTTP/CONNECT 直连。增量、Gateway 本地容量等待使用;`DIRECT` 仍先经过 TargetPolicy 再建立 HTTP/CONNECT 直连。`proxy-gateway`
进程装配、Outcome 与 Checker 闭环尚未实现。`ReportOutcomes` 仍明确返回 `Unimplemented` 已装配 Register/Watch/ACK/Runtime 会话、HTTP 代理监听和 Snapshot 就绪探针;控制面中断时
保持进程运行并以有界退避重连,未取得有效 Snapshot 的 Worker 不会 Ready。增量、带凭据
Outcome 与 Checker 闭环尚未实现。`ReportOutcomes` 仍明确返回 `Unimplemented`
`100,000 QPS` 仍是未验证的设计目标。 `100,000 QPS` 仍是未验证的设计目标。
`WatchSnapshots` 建立时校验当前 session每次签发快照引用时也把 `session_id` `WatchSnapshots` 建立时校验当前 session每次签发快照引用时也把 `session_id`
@ -162,4 +165,21 @@ Routing 决定 AVAILABLE、SUSPECT 或 UNHEALTHY并更新 Redis 活动池,
集群环境使用 mTLS证书身份绑定 Worker/Checker 类型和环境。服务端校验 集群环境使用 mTLS证书身份绑定 Worker/Checker 类型和环境。服务端校验
消息中的逻辑 ID 与证书授权一致,设置单消息大小、流持续时间、并发 Stream 消息中的逻辑 ID 与证书授权一致,设置单消息大小、流持续时间、并发 Stream
和上报批次上限。`secret_ref` 是受控引用,不在 Proto 中传播真实密码。 和上报批次上限。`secret_ref` 是受控引用;完整 Snapshot 的 `credentials` 在 mTLS 会话中
携带引用对应材料Controller 和 Gateway 仅在内存处理,禁止写入 Redis/PostgreSQL、日志或指标。
## 9. Gateway 启动参数
Gateway 不复用 `controlPlane.listen` 作为客户端地址。`listen` 是 Controller 的
服务端绑定地址;每个 Gateway 必须显式提供以下独立参数或同名环境变量:
- `-control-plane` / `PROXY_POOL_CONTROL_PLANE_ADDRESS`Controller 的可拨号地址。
- `-cluster-id` / `PROXY_POOL_CLUSTER_ID`:快照所属集群。
- `-worker-id` / `PROXY_POOL_WORKER_ID`:唯一逻辑 Worker。
- `-instance-id` / `PROXY_POOL_INSTANCE_ID`:唯一进程实例。
- `-zone` / `PROXY_POOL_ZONE`:实例可用区。
`controlPlane.tls.mode=mtls`Gateway 使用 `controlPlane.gatewayTLS` 中独立的
客户端证书、私钥和 Controller CA 发起 TLS 1.3 连接;证书必须符合 Controller 的
SPIFFE Worker 身份校验。
`disabled` 仅接受回环控制面地址,供本地 fixture 使用。

View File

@ -21,8 +21,9 @@ go run ./cmd/proxy-controller -config CONFIG_FILE
所有 Controller 副本必须一致,且该密钥不得放入 YAML 或 PostgreSQL。该入口已装配 所有 Controller 副本必须一致,且该密钥不得放入 YAML 或 PostgreSQL。该入口已装配
PostgreSQL 管理面迁移、Redis 活动池、Distribution/Admin 独立监听与优雅停机; PostgreSQL 管理面迁移、Redis 活动池、Distribution/Admin 独立监听与优雅停机;
Controller Metrics 独立监听、`/livez`、`/readyz` 和基础 Prometheus 运行时指标; Controller Metrics 独立监听、`/livez`、`/readyz` 和基础 Prometheus 运行时指标;
Provider 自动补池、分布式配额、动态重载和 Admin 低基数统计已装配。完整 Gateway、 Provider 自动补池、分布式配额、动态重载和 Admin 低基数统计已装配。Gateway 进程、
Checker、Worker 控制面与代表性负载验证仍在后续实施范围。 Worker 控制面会话和 Snapshot 就绪探针已装配Checker、带凭据 Proxy 分发与代表性
负载验证仍在后续实施范围。
所有时间值使用 Go duration例如 `500ms`、`30s`、`5m`。示例中的 所有时间值使用 Go duration例如 `500ms`、`30s`、`5m`。示例中的
`${TOKEN}`、`${PASSWORD}`、`${POSTGRES_URL}` 等由加载器从同名环境变量 `${TOKEN}`、`${PASSWORD}`、`${POSTGRES_URL}` 等由加载器从同名环境变量
@ -166,6 +167,10 @@ controlPlane:
clientCAFile: /run/secrets/worker-ca.pem clientCAFile: /run/secrets/worker-ca.pem
trustDomain: proxy.example trustDomain: proxy.example
environment: production environment: production
gatewayTLS:
certFile: /run/secrets/gateway-cert.pem
keyFile: /run/secrets/gateway-key.pem
serverCAFile: /run/secrets/controller-ca.pem
``` ```
- `protocolVersion` 当前固定为 `1` - `protocolVersion` 当前固定为 `1`
@ -175,6 +180,15 @@ controlPlane:
- `tls.mode` 只能为 `disabled``mtls`。`disabled` 只允许回环监听;`mtls` 必须 - `tls.mode` 只能为 `disabled``mtls`。`disabled` 只允许回环监听;`mtls` 必须
同时配置证书、私钥、客户端 CA、全小写 DNS `trustDomain` 和单 URI 路径段 同时配置证书、私钥、客户端 CA、全小写 DNS `trustDomain` 和单 URI 路径段
`environment` `environment`
- `gatewayTLS` 是 Gateway 的客户端证书、私钥和 Controller CA`tls` 的服务端
证书和 Worker CA 分离。三项可以同时省略(未运行 Gateway配置任一项时必须完整提供。
Gateway 连接 Controller 时使用独立启动参数而非 `controlPlane.listen`。至少设置
`PROXY_POOL_CONTROL_PLANE_ADDRESS`、`PROXY_POOL_CLUSTER_ID`、
`PROXY_POOL_WORKER_ID`、`PROXY_POOL_INSTANCE_ID` 和 `PROXY_POOL_ZONE`,详见
[控制面协议](../api/control-plane.md#9-gateway-启动参数)。基础 Compose/Kubernetes
模板保持 `controlPlane.enabled: false`,环境 Overlay 挂载 mTLS 证书并启用后才可启动
Gateway。
## 5. Gateway ## 5. Gateway

View File

@ -12,7 +12,7 @@ proxy-pool/
├── internal/ ├── internal/
│ ├── config/ # 严格配置解析和校验 │ ├── config/ # 严格配置解析和校验
│ ├── domain/ # 无传输、无存储依赖的领域模型 │ ├── domain/ # 无传输、无存储依赖的领域模型
│ ├── gateway/ # snapshot、dispatch、server、transport │ ├── gateway/ # bootstrap、snapshot、dispatch、server、transport
│ ├── controller/ # provider、pool、extraction、operations、runtime、bootstrap │ ├── controller/ # provider、pool、extraction、operations、runtime、bootstrap
│ ├── adapters/ # PostgreSQL、Redis、Provider API、内存适配 │ ├── adapters/ # PostgreSQL、Redis、Provider API、内存适配
│ └── platform/ # HTTP、安全、日志、指标、停机和进程装配 │ └── platform/ # HTTP、安全、日志、指标、停机和进程装配
@ -34,6 +34,10 @@ proxy-pool/
选择、session 与重试资格等热路径决策;`transport` 独占连接池、上游握手和 选择、session 与重试资格等热路径决策;`transport` 独占连接池、上游握手和
隧道生命周期。任何包都不得从热路径反向调用 Controller 存储。 隧道生命周期。任何包都不得从热路径反向调用 Controller 存储。
`gateway/bootstrap` 只在进程启动时加载配置、建立 gRPC 控制面 Session、组装 HTTP
代理与 Metrics 监听;请求热路径只读取本地 Snapshot。`/readyz` 要求当前 Snapshot
尚未到期,控制面断开期间以有界退避重连而不访问 Redis/PostgreSQL。
### proxy-controller ### proxy-controller
Controller 是首版模块化单体。Provider、Pool、Routing 和 Extraction 共享 Controller 是首版模块化单体。Provider、Pool、Routing 和 Extraction 共享

View File

@ -212,16 +212,18 @@ Prometheus 运行时指标,三监听器隔离已通过测试;业务指标仍
WorkerControlPlane 现已接入 Controller 生命周期Register、ACK 和 Runtime WorkerControlPlane 现已接入 Controller 生命周期Register、ACK 和 Runtime
报告均经 Redis 服务端 TTL 的 session/issued-snapshot/ACK 栅栏校验mTLS SPIFFE 报告均经 Redis 服务端 TTL 的 session/issued-snapshot/ACK 栅栏校验mTLS SPIFFE
身份、消息/流限制和有界停机已实现。`WatchSnapshots` 会发送当前 epoch 的基础完整 身份、消息/流限制和有界停机已实现。`WatchSnapshots` 会发送当前 epoch 的基础完整
Snapshot 并保持连接Gateway 已具备 Register/Watch/ACK/Runtime 会话协调组件。 Snapshot 并保持连接Gateway 已具备 Register/Watch/ACK/Runtime 会话协调组件与
按 Worker 的可下发 ownership 索引已进入 Redis 原子脚本,并可构建无凭据引用的 `proxy-gateway` 进程装配。
已归属 Proxy payload。Snapshot 签发与 session 匹配在同一 Redis Lua 操作中完成, 按 Worker 的可下发 ownership 索引已进入 Redis 原子脚本,并可构建已归属 Proxy、
去重凭据材料和 Routing 的完整 payload。Snapshot 签发与 session 匹配在同一 Redis Lua 操作中完成,
重注册会清除旧引用,避免迟到 Stream 覆盖新 session。Worker 服务端会在最近完整 重注册会清除旧引用,避免迟到 Stream 覆盖新 session。Worker 服务端会在最近完整
Snapshot 的 `valid_until` 到达时结束流;公用 `SessionSupervisor` 已为 Gateway 调用方 Snapshot 的 `valid_until` 到达时结束流;公用 `SessionSupervisor` 已为 Gateway 调用方
提供可恢复错误的有界指数退避重连,并在参数/认证/协议错误时停止。Gateway Routing 提供可恢复错误的有界指数退避重连,并在参数/认证/协议错误时停止。Gateway Routing
payload 已按配置顺序和 Admin revision/current 状态发布并覆盖 checksumGateway 已将其与 payload 已按配置顺序和 Admin revision/current 状态发布并覆盖 checksumGateway 已将其与
Proxy 原子编译为同版本 View动态 Router 只匹配该未过期 View。派发器的五种上游选择已 Proxy 原子编译为同版本 View动态 Router 只匹配该未过期 View。派发器的五种上游选择已
接入该 View并在容量耗尽时在同版本候选中回退`onUnavailable` 的 reject、wait 与 direct 接入该 View并在容量耗尽时在同版本候选中回退`onUnavailable` 的 reject、wait 与 direct
已接入 Gateway凭据分发、Gateway 命令与 Outcome 上报仍未实现。 已接入 Gateway`proxy-gateway` 已装配本地 HTTP/Metrics 监听、快照就绪探针和
控制面重连与快照凭据分发Outcome 上报仍未实现。
已新增公用 `domain/activitypool` 契约及并发安全内存参考实现Provider 已新增公用 `domain/activitypool` 契约及并发安全内存参考实现Provider
Reconciler 通过 `UpsertFetched` 写入带供应商 TTL 和分配安全余量的批次;已覆盖 Reconciler 通过 `UpsertFetched` 写入带供应商 TTL 和分配安全余量的批次;已覆盖
@ -258,7 +260,7 @@ Controller 多副本共享同一计数。Client 身份只以 SHA-256 摘要进
原子回收历史字段Redis 异常 fail-closed 并返回 503真实额度耗尽返回 429。 原子回收历史字段Redis 异常 fail-closed 并返回 503真实额度耗尽返回 429。
Gateway 请求热路径仍只使用本地准入,不增加 Redis/PostgreSQL 调用。 Gateway 请求热路径仍只使用本地准入,不增加 Redis/PostgreSQL 调用。
WorkerControlPlane gRPC 接收端、session 签发/心跳、Snapshot ACK 账本、基础 WorkerControlPlane gRPC 接收端、session 签发/心跳、Snapshot ACK 账本、基础
Snapshot 流和 Gateway 会话客户端已完成;权威 Proxy/Routing 发布、凭据分发 Snapshot 流、Gateway 会话客户端与快照凭据分发已完成;权威 Proxy/Routing 发布
Outcome 和健康执行链仍待完成,因此 Task 10 尚未全部完成。 Outcome 和健康执行链仍待完成,因此 Task 10 尚未全部完成。
## Task 11: Checker and Health Reducer ## Task 11: Checker and Health Reducer

View File

@ -37,7 +37,9 @@
- `OPS-001`:完整 Snapshot 目标、epoch/version、校验和验证及原子替换。 - `OPS-001`:完整 Snapshot 目标、epoch/version、校验和验证及原子替换。
- `CAP-001 / GW 热路径边界`:本地 Dispatch 条件过滤与原子容量预留。 - `CAP-001 / GW 热路径边界`:本地 Dispatch 条件过滤与原子容量预留。
- `GW-*`HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、超时、重试、保护链与 - `GW-*`HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、超时、重试、保护链与
优雅停机 Handler 已实现并通过定向测试。 优雅停机 Handler 已实现并通过定向测试。`proxy-gateway` 已装配本地 HTTP/Metrics
监听、快照就绪探针、显式控制面拨号和 Register/Watch/ACK/Runtime 会话;本地 gRPC
集成测试验证收到有效 Snapshot 后才 Ready并可完成 direct 路由。
- `PROVIDER-*`Provider HTTP Client、严格响应上限、模板解析安全边界、凭据 - `PROVIDER-*`Provider HTTP Client、严格响应上限、模板解析安全边界、凭据
引用 Store 与 Reconciler Adapter 已实现。 引用 Store 与 Reconciler Adapter 已实现。
- `DIST/Admin HTTP`:严格 JSON、Request ID、Problem 响应及 Distribution/Admin - `DIST/Admin HTTP`:严格 JSON、Request ID、Problem 响应及 Distribution/Admin
@ -51,9 +53,10 @@
服务端 TTL负向 ACK 会关闭 Runtime 写入栅栏。Controller 已装配 loopback 服务端 TTL负向 ACK 会关闭 Runtime 写入栅栏。Controller 已装配 loopback
plaintext fixture 与 SPIFFE mTLS 服务端;基础 Snapshot 流和 Gateway 的 plaintext fixture 与 SPIFFE mTLS 服务端;基础 Snapshot 流和 Gateway 的
Register/Watch/ACK/Runtime 会话协调已实现。Redis 以 Worker 可下发 ownership Register/Watch/ACK/Runtime 会话协调已实现。Redis 以 Worker 可下发 ownership
索引构建无凭据引用的已归属 Proxy payload并以租约收紧可用期Gateway Routing 索引构建已归属 Proxy payload并以租约收紧可用期凭据材料按引用去重并与 Proxy/
payload 已由配置顺序和 Admin 当前状态合成并覆盖 Snapshot checksum其运行时消费、 Routing 一起纳入 Snapshot checksum仅在 mTLS 和 Gateway 当前内存 View 中处理。Gateway
凭据分发、Outcome 和 Checker 尚未闭环。Snapshot 签发在 Redis 中原子匹配当前 Routing payload 已由配置顺序和 Admin 当前状态合成并覆盖 Snapshot checksum其运行时消费、
Outcome 和 Checker 尚未闭环。Snapshot 签发在 Redis 中原子匹配当前
`session_id`,重注册会清除旧引用,迟到旧 Stream 不会覆盖新 session。Controller `session_id`,重注册会清除旧引用,迟到旧 Stream 不会覆盖新 session。Controller
在最近成功下发的 Snapshot `valid_until` 到达时关闭流Gateway 的公用 在最近成功下发的 Snapshot `valid_until` 到达时关闭流Gateway 的公用
`SessionSupervisor` 已实现可恢复错误的有界退避重连。Gateway 会校验并执行 Snapshot `SessionSupervisor` 已实现可恢复错误的有界退避重连。Gateway 会校验并执行 Snapshot
@ -88,10 +91,10 @@ CI 已配置 Linux race job。PostgreSQL 18 和 Redis 8.2 的隔离 Adapter fixt
以下已有设计、接口或部署位置,但尚无端到端生产实现: 以下已有设计、接口或部署位置,但尚无端到端生产实现:
1. `cmd/proxy-gateway/checker/loadgen` 进程装配;`proxy-controller` 已完成 1. `cmd/proxy-checker/loadgen` 进程装配;`proxy-controller` 已完成
Admin/Distribution/Metrics 与 PostgreSQL/Redis 启动装配,但 Provider 和业务 Admin/Distribution/Metrics 与 PostgreSQL/Redis 启动装配,`proxy-gateway` 已完成
指标链未闭环。 HTTP/Metrics 与控制面 Session 装配,但 Provider 和业务指标链未闭环。
2. Gateway 进程装配、生产连接池调优与代表性流量压测。 2. Gateway 的带凭据 Proxy 分发、生产连接池调优与代表性流量压测。
3. Provider 分布式 singleflight/Leader、长期凭据回收和累计额度执行器。 3. Provider 分布式 singleflight/Leader、长期凭据回收和累计额度执行器。
4. Controller 的 PostgreSQL 连接池、迁移和 pgx Adapter 启动装配已完成; 4. Controller 的 PostgreSQL 连接池、迁移和 pgx Adapter 启动装配已完成;
公用 bootstrap 已通过 PostgreSQL 18 + Redis 8.2 双存储集成Controller 公用 bootstrap 已通过 PostgreSQL 18 + Redis 8.2 双存储集成Controller
@ -99,9 +102,9 @@ CI 已配置 Linux race job。PostgreSQL 18 和 Redis 8.2 的隔离 Adapter fixt
5. Worker Register/ACK/Runtime gRPC 接收路径、Redis 服务端 TTL 会话生命周期、 5. Worker Register/ACK/Runtime gRPC 接收路径、Redis 服务端 TTL 会话生命周期、
SPIFFE mTLS 校验和 Controller 生命周期接线已完成Redis Provider Leader、 SPIFFE mTLS 校验和 Controller 生命周期接线已完成Redis Provider Leader、
分布式请求额度、Distribution Client 限制和 Provider 状态丢失重建已完成。 分布式请求额度、Distribution Client 限制和 Provider 状态丢失重建已完成。
6. Worker 基础网络快照流、无凭据引用 Proxy/Gateway Routing payload、Gateway Snapshot 6. Worker 基础网络快照流、Proxy/Gateway Routing/凭据 Snapshot payload、Gateway Snapshot
客户端、同版本 Routing 编译/动态匹配、五种策略上游选择与 reject/wait/direct 已完成;凭据 客户端和进程装配、同版本 Routing 编译/动态匹配、五种策略上游选择与 reject/wait/direct 已完成;
分发与 Outcome 上报仍待完成。Redis ownership drain/ACK/过期回收及按 Worker 的可下发索引已完成。 Outcome 上报仍待完成。Redis ownership drain/ACK/过期回收及按 Worker 的可下发索引已完成。
7. Checker 调度、探测器和健康 reducer。 7. Checker 调度、探测器和健康 reducer。
8. Admin/Distribution 细粒度授权和审计查询Distribution 分布式限流已完成。 8. Admin/Distribution 细粒度授权和审计查询Distribution 分布式限流已完成。
9. 真实 Compose/Kubernetes 集成、故障演练和代表性集群负载测试。 9. 真实 Compose/Kubernetes 集成、故障演练和代表性集群负载测试。

View File

@ -7,9 +7,9 @@
| ID | 最终需求 | 来源 | 验证证据 | | ID | 最终需求 | 来源 | 验证证据 |
|---|---|---|---| |---|---|---|---|
| ARCH-001 | 数据面 Worker 与控制面 Controller 分离 | 1-70 | 包、协议和部署拓扑已分离Controller 已运行 Worker Register/Watch/ACK/Runtime gRPC并发布无凭据 Proxy/Gateway Routing 完整快照Gateway 已将同版本 Routing 编译为动态匹配 View。Gateway/Checker/Loadgen 构建产物、策略派发与 `onUnavailable` 待实现 | | ARCH-001 | 数据面 Worker 与控制面 Controller 分离 | 1-70 | 包、协议和部署拓扑已分离Controller 已运行 Worker Register/Watch/ACK/Runtime gRPC并发布 Proxy/Gateway Routing/按引用去重凭据完整快照Gateway 已将它们编译为同版本动态 View并由独立进程维护控制面会话。Checker/Loadgen 构建产物待实现 |
| ARCH-002 | 热路径只做认证、本地路由和网络转发 | 1-70, 380-430 | Snapshot/Dispatch 及依赖边界已验证完整 Gateway 进程与代表性性能剖析待完成 | | ARCH-002 | 热路径只做认证、本地路由和网络转发 | 1-70, 380-430 | Gateway bootstrap 集成测试验证启动期控制面会话与快照就绪HTTP 请求只走本地 Snapshot/Dispatch代表性性能剖析待完成 |
| ARCH-003 | Gateway、Distribution、Admin、Metrics 独立入口 | 8904-8958 | Controller 命令已装配 Distribution/Admin/Metrics 三个独立监听及联动停机Gateway 生产入口待装配 | | ARCH-003 | Gateway、Distribution、Admin、Metrics 独立入口 | 8904-8958 | Controller 命令已装配 Distribution/Admin/Metrics 三个独立监听及联动停机Gateway 命令已装配代理与 Metrics 监听,运行时 mTLS 部署 Overlay 待完成 |
| ARCH-004 | Controller 集中 Provider 获取与切换 | 1403-1580 | Redis Leader、动态 Provider Supervisor 与 Bootstrap 生产装配已完成Admin disable/reload 驱动取消替换,多副本按权威 HMAC 指纹和 revision 栅栏收敛并拒绝旧配置换主Routing 切换到 Drain 的编排待完成 | | ARCH-004 | Controller 集中 Provider 获取与切换 | 1403-1580 | Redis Leader、动态 Provider Supervisor 与 Bootstrap 生产装配已完成Admin disable/reload 驱动取消替换,多副本按权威 HMAC 指纹和 revision 栅栏收敛并拒绝旧配置换主Routing 切换到 Drain 的编排待完成 |
| ARCH-005 | 100k QPS 峰值使用多 Worker 集群 | 当前会话 | 未验证设计目标;待代表性集群负载报告 | | ARCH-005 | 100k QPS 峰值使用多 Worker 集群 | 当前会话 | 未验证设计目标;待代表性集群负载报告 |

View File

@ -604,8 +604,11 @@ type WorkerSnapshot struct {
Checksum []byte `protobuf:"bytes,5,opt,name=checksum,proto3" json:"checksum,omitempty"` Checksum []byte `protobuf:"bytes,5,opt,name=checksum,proto3" json:"checksum,omitempty"`
Routing []*RoutingRule `protobuf:"bytes,6,rep,name=routing,proto3" json:"routing,omitempty"` Routing []*RoutingRule `protobuf:"bytes,6,rep,name=routing,proto3" json:"routing,omitempty"`
Proxies []*OwnedProxy `protobuf:"bytes,7,rep,name=proxies,proto3" json:"proxies,omitempty"` Proxies []*OwnedProxy `protobuf:"bytes,7,rep,name=proxies,proto3" json:"proxies,omitempty"`
unknownFields protoimpl.UnknownFields // Credential materials are protected by the WorkerControlPlane mTLS session
sizeCache protoimpl.SizeCache // and retained only in the Gateway's current in-memory snapshot view.
Credentials []*SnapshotCredential `protobuf:"bytes,14,rep,name=credentials,proto3" json:"credentials,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *WorkerSnapshot) Reset() { func (x *WorkerSnapshot) Reset() {
@ -687,6 +690,81 @@ func (x *WorkerSnapshot) GetProxies() []*OwnedProxy {
return nil return nil
} }
func (x *WorkerSnapshot) GetCredentials() []*SnapshotCredential {
if x != nil {
return x.Credentials
}
return nil
}
type SnapshotCredential struct {
state protoimpl.MessageState `protogen:"open.v1"`
SecretRef string `protobuf:"bytes,1,opt,name=secret_ref,json=secretRef,proto3" json:"secret_ref,omitempty"`
CredentialVersion string `protobuf:"bytes,2,opt,name=credential_version,json=credentialVersion,proto3" json:"credential_version,omitempty"`
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SnapshotCredential) Reset() {
*x = SnapshotCredential{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SnapshotCredential) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SnapshotCredential) ProtoMessage() {}
func (x *SnapshotCredential) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SnapshotCredential.ProtoReflect.Descriptor instead.
func (*SnapshotCredential) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{5}
}
func (x *SnapshotCredential) GetSecretRef() string {
if x != nil {
return x.SecretRef
}
return ""
}
func (x *SnapshotCredential) GetCredentialVersion() string {
if x != nil {
return x.CredentialVersion
}
return ""
}
func (x *SnapshotCredential) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *SnapshotCredential) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
type SnapshotDelta struct { type SnapshotDelta struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
BaseVersion uint64 `protobuf:"varint,1,opt,name=base_version,json=baseVersion,proto3" json:"base_version,omitempty"` BaseVersion uint64 `protobuf:"varint,1,opt,name=base_version,json=baseVersion,proto3" json:"base_version,omitempty"`
@ -704,7 +782,7 @@ type SnapshotDelta struct {
func (x *SnapshotDelta) Reset() { func (x *SnapshotDelta) Reset() {
*x = SnapshotDelta{} *x = SnapshotDelta{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[5] mi := &file_controlplane_v1_controlplane_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -716,7 +794,7 @@ func (x *SnapshotDelta) String() string {
func (*SnapshotDelta) ProtoMessage() {} func (*SnapshotDelta) ProtoMessage() {}
func (x *SnapshotDelta) ProtoReflect() protoreflect.Message { func (x *SnapshotDelta) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[5] mi := &file_controlplane_v1_controlplane_proto_msgTypes[6]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -729,7 +807,7 @@ func (x *SnapshotDelta) ProtoReflect() protoreflect.Message {
// Deprecated: Use SnapshotDelta.ProtoReflect.Descriptor instead. // Deprecated: Use SnapshotDelta.ProtoReflect.Descriptor instead.
func (*SnapshotDelta) Descriptor() ([]byte, []int) { func (*SnapshotDelta) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{5} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{6}
} }
func (x *SnapshotDelta) GetBaseVersion() uint64 { func (x *SnapshotDelta) GetBaseVersion() uint64 {
@ -813,7 +891,7 @@ type RoutingRule struct {
func (x *RoutingRule) Reset() { func (x *RoutingRule) Reset() {
*x = RoutingRule{} *x = RoutingRule{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[6] mi := &file_controlplane_v1_controlplane_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -825,7 +903,7 @@ func (x *RoutingRule) String() string {
func (*RoutingRule) ProtoMessage() {} func (*RoutingRule) ProtoMessage() {}
func (x *RoutingRule) ProtoReflect() protoreflect.Message { func (x *RoutingRule) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[6] mi := &file_controlplane_v1_controlplane_proto_msgTypes[7]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -838,7 +916,7 @@ func (x *RoutingRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoutingRule.ProtoReflect.Descriptor instead. // Deprecated: Use RoutingRule.ProtoReflect.Descriptor instead.
func (*RoutingRule) Descriptor() ([]byte, []int) { func (*RoutingRule) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{6} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{7}
} }
func (x *RoutingRule) GetName() string { func (x *RoutingRule) GetName() string {
@ -922,7 +1000,7 @@ type RoutingStrategy struct {
func (x *RoutingStrategy) Reset() { func (x *RoutingStrategy) Reset() {
*x = RoutingStrategy{} *x = RoutingStrategy{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[7] mi := &file_controlplane_v1_controlplane_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -934,7 +1012,7 @@ func (x *RoutingStrategy) String() string {
func (*RoutingStrategy) ProtoMessage() {} func (*RoutingStrategy) ProtoMessage() {}
func (x *RoutingStrategy) ProtoReflect() protoreflect.Message { func (x *RoutingStrategy) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[7] mi := &file_controlplane_v1_controlplane_proto_msgTypes[8]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -947,7 +1025,7 @@ func (x *RoutingStrategy) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoutingStrategy.ProtoReflect.Descriptor instead. // Deprecated: Use RoutingStrategy.ProtoReflect.Descriptor instead.
func (*RoutingStrategy) Descriptor() ([]byte, []int) { func (*RoutingStrategy) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{7} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{8}
} }
func (x *RoutingStrategy) GetType() StrategyType { func (x *RoutingStrategy) GetType() StrategyType {
@ -992,7 +1070,7 @@ type OwnedProxy struct {
func (x *OwnedProxy) Reset() { func (x *OwnedProxy) Reset() {
*x = OwnedProxy{} *x = OwnedProxy{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[8] mi := &file_controlplane_v1_controlplane_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1004,7 +1082,7 @@ func (x *OwnedProxy) String() string {
func (*OwnedProxy) ProtoMessage() {} func (*OwnedProxy) ProtoMessage() {}
func (x *OwnedProxy) ProtoReflect() protoreflect.Message { func (x *OwnedProxy) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[8] mi := &file_controlplane_v1_controlplane_proto_msgTypes[9]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1017,7 +1095,7 @@ func (x *OwnedProxy) ProtoReflect() protoreflect.Message {
// Deprecated: Use OwnedProxy.ProtoReflect.Descriptor instead. // Deprecated: Use OwnedProxy.ProtoReflect.Descriptor instead.
func (*OwnedProxy) Descriptor() ([]byte, []int) { func (*OwnedProxy) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{8} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{9}
} }
func (x *OwnedProxy) GetId() string { func (x *OwnedProxy) GetId() string {
@ -1127,7 +1205,7 @@ type AcknowledgeSnapshotRequest struct {
func (x *AcknowledgeSnapshotRequest) Reset() { func (x *AcknowledgeSnapshotRequest) Reset() {
*x = AcknowledgeSnapshotRequest{} *x = AcknowledgeSnapshotRequest{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[9] mi := &file_controlplane_v1_controlplane_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1139,7 +1217,7 @@ func (x *AcknowledgeSnapshotRequest) String() string {
func (*AcknowledgeSnapshotRequest) ProtoMessage() {} func (*AcknowledgeSnapshotRequest) ProtoMessage() {}
func (x *AcknowledgeSnapshotRequest) ProtoReflect() protoreflect.Message { func (x *AcknowledgeSnapshotRequest) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[9] mi := &file_controlplane_v1_controlplane_proto_msgTypes[10]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1152,7 +1230,7 @@ func (x *AcknowledgeSnapshotRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AcknowledgeSnapshotRequest.ProtoReflect.Descriptor instead. // Deprecated: Use AcknowledgeSnapshotRequest.ProtoReflect.Descriptor instead.
func (*AcknowledgeSnapshotRequest) Descriptor() ([]byte, []int) { func (*AcknowledgeSnapshotRequest) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{9} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{10}
} }
func (x *AcknowledgeSnapshotRequest) GetWorkerId() string { func (x *AcknowledgeSnapshotRequest) GetWorkerId() string {
@ -1223,7 +1301,7 @@ type OutcomeBatch struct {
func (x *OutcomeBatch) Reset() { func (x *OutcomeBatch) Reset() {
*x = OutcomeBatch{} *x = OutcomeBatch{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[10] mi := &file_controlplane_v1_controlplane_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1235,7 +1313,7 @@ func (x *OutcomeBatch) String() string {
func (*OutcomeBatch) ProtoMessage() {} func (*OutcomeBatch) ProtoMessage() {}
func (x *OutcomeBatch) ProtoReflect() protoreflect.Message { func (x *OutcomeBatch) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[10] mi := &file_controlplane_v1_controlplane_proto_msgTypes[11]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1248,7 +1326,7 @@ func (x *OutcomeBatch) ProtoReflect() protoreflect.Message {
// Deprecated: Use OutcomeBatch.ProtoReflect.Descriptor instead. // Deprecated: Use OutcomeBatch.ProtoReflect.Descriptor instead.
func (*OutcomeBatch) Descriptor() ([]byte, []int) { func (*OutcomeBatch) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{10} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{11}
} }
func (x *OutcomeBatch) GetWorkerId() string { func (x *OutcomeBatch) GetWorkerId() string {
@ -1294,7 +1372,7 @@ type ProxyOutcome struct {
func (x *ProxyOutcome) Reset() { func (x *ProxyOutcome) Reset() {
*x = ProxyOutcome{} *x = ProxyOutcome{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[11] mi := &file_controlplane_v1_controlplane_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1306,7 +1384,7 @@ func (x *ProxyOutcome) String() string {
func (*ProxyOutcome) ProtoMessage() {} func (*ProxyOutcome) ProtoMessage() {}
func (x *ProxyOutcome) ProtoReflect() protoreflect.Message { func (x *ProxyOutcome) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[11] mi := &file_controlplane_v1_controlplane_proto_msgTypes[12]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1319,7 +1397,7 @@ func (x *ProxyOutcome) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProxyOutcome.ProtoReflect.Descriptor instead. // Deprecated: Use ProxyOutcome.ProtoReflect.Descriptor instead.
func (*ProxyOutcome) Descriptor() ([]byte, []int) { func (*ProxyOutcome) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{11} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{12}
} }
func (x *ProxyOutcome) GetProxyId() string { func (x *ProxyOutcome) GetProxyId() string {
@ -1380,7 +1458,7 @@ type ReportOutcomesResponse struct {
func (x *ReportOutcomesResponse) Reset() { func (x *ReportOutcomesResponse) Reset() {
*x = ReportOutcomesResponse{} *x = ReportOutcomesResponse{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[12] mi := &file_controlplane_v1_controlplane_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1392,7 +1470,7 @@ func (x *ReportOutcomesResponse) String() string {
func (*ReportOutcomesResponse) ProtoMessage() {} func (*ReportOutcomesResponse) ProtoMessage() {}
func (x *ReportOutcomesResponse) ProtoReflect() protoreflect.Message { func (x *ReportOutcomesResponse) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[12] mi := &file_controlplane_v1_controlplane_proto_msgTypes[13]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1405,7 +1483,7 @@ func (x *ReportOutcomesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReportOutcomesResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ReportOutcomesResponse.ProtoReflect.Descriptor instead.
func (*ReportOutcomesResponse) Descriptor() ([]byte, []int) { func (*ReportOutcomesResponse) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{12} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{13}
} }
func (x *ReportOutcomesResponse) GetAcceptedThroughSequence() uint64 { func (x *ReportOutcomesResponse) GetAcceptedThroughSequence() uint64 {
@ -1430,7 +1508,7 @@ type ReportRuntimeRequest struct {
func (x *ReportRuntimeRequest) Reset() { func (x *ReportRuntimeRequest) Reset() {
*x = ReportRuntimeRequest{} *x = ReportRuntimeRequest{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[13] mi := &file_controlplane_v1_controlplane_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1442,7 +1520,7 @@ func (x *ReportRuntimeRequest) String() string {
func (*ReportRuntimeRequest) ProtoMessage() {} func (*ReportRuntimeRequest) ProtoMessage() {}
func (x *ReportRuntimeRequest) ProtoReflect() protoreflect.Message { func (x *ReportRuntimeRequest) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[13] mi := &file_controlplane_v1_controlplane_proto_msgTypes[14]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1455,7 +1533,7 @@ func (x *ReportRuntimeRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReportRuntimeRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ReportRuntimeRequest.ProtoReflect.Descriptor instead.
func (*ReportRuntimeRequest) Descriptor() ([]byte, []int) { func (*ReportRuntimeRequest) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{13} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{14}
} }
func (x *ReportRuntimeRequest) GetWorkerId() string { func (x *ReportRuntimeRequest) GetWorkerId() string {
@ -1519,7 +1597,7 @@ type ProxyRuntime struct {
func (x *ProxyRuntime) Reset() { func (x *ProxyRuntime) Reset() {
*x = ProxyRuntime{} *x = ProxyRuntime{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[14] mi := &file_controlplane_v1_controlplane_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1531,7 +1609,7 @@ func (x *ProxyRuntime) String() string {
func (*ProxyRuntime) ProtoMessage() {} func (*ProxyRuntime) ProtoMessage() {}
func (x *ProxyRuntime) ProtoReflect() protoreflect.Message { func (x *ProxyRuntime) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[14] mi := &file_controlplane_v1_controlplane_proto_msgTypes[15]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1544,7 +1622,7 @@ func (x *ProxyRuntime) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProxyRuntime.ProtoReflect.Descriptor instead. // Deprecated: Use ProxyRuntime.ProtoReflect.Descriptor instead.
func (*ProxyRuntime) Descriptor() ([]byte, []int) { func (*ProxyRuntime) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{14} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{15}
} }
func (x *ProxyRuntime) GetProxyId() string { func (x *ProxyRuntime) GetProxyId() string {
@ -1586,7 +1664,7 @@ type ReportRuntimeResponse struct {
func (x *ReportRuntimeResponse) Reset() { func (x *ReportRuntimeResponse) Reset() {
*x = ReportRuntimeResponse{} *x = ReportRuntimeResponse{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[15] mi := &file_controlplane_v1_controlplane_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1598,7 +1676,7 @@ func (x *ReportRuntimeResponse) String() string {
func (*ReportRuntimeResponse) ProtoMessage() {} func (*ReportRuntimeResponse) ProtoMessage() {}
func (x *ReportRuntimeResponse) ProtoReflect() protoreflect.Message { func (x *ReportRuntimeResponse) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[15] mi := &file_controlplane_v1_controlplane_proto_msgTypes[16]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1611,7 +1689,7 @@ func (x *ReportRuntimeResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReportRuntimeResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ReportRuntimeResponse.ProtoReflect.Descriptor instead.
func (*ReportRuntimeResponse) Descriptor() ([]byte, []int) { func (*ReportRuntimeResponse) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{15} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{16}
} }
func (x *ReportRuntimeResponse) GetAcceptedOwnershipEpoch() uint64 { func (x *ReportRuntimeResponse) GetAcceptedOwnershipEpoch() uint64 {
@ -1647,7 +1725,7 @@ type StreamCheckTasksRequest struct {
func (x *StreamCheckTasksRequest) Reset() { func (x *StreamCheckTasksRequest) Reset() {
*x = StreamCheckTasksRequest{} *x = StreamCheckTasksRequest{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[16] mi := &file_controlplane_v1_controlplane_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1659,7 +1737,7 @@ func (x *StreamCheckTasksRequest) String() string {
func (*StreamCheckTasksRequest) ProtoMessage() {} func (*StreamCheckTasksRequest) ProtoMessage() {}
func (x *StreamCheckTasksRequest) ProtoReflect() protoreflect.Message { func (x *StreamCheckTasksRequest) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[16] mi := &file_controlplane_v1_controlplane_proto_msgTypes[17]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1672,7 +1750,7 @@ func (x *StreamCheckTasksRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use StreamCheckTasksRequest.ProtoReflect.Descriptor instead. // Deprecated: Use StreamCheckTasksRequest.ProtoReflect.Descriptor instead.
func (*StreamCheckTasksRequest) Descriptor() ([]byte, []int) { func (*StreamCheckTasksRequest) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{16} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{17}
} }
func (x *StreamCheckTasksRequest) GetCheckerId() string { func (x *StreamCheckTasksRequest) GetCheckerId() string {
@ -1723,7 +1801,7 @@ type CheckTask struct {
func (x *CheckTask) Reset() { func (x *CheckTask) Reset() {
*x = CheckTask{} *x = CheckTask{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[17] mi := &file_controlplane_v1_controlplane_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1735,7 +1813,7 @@ func (x *CheckTask) String() string {
func (*CheckTask) ProtoMessage() {} func (*CheckTask) ProtoMessage() {}
func (x *CheckTask) ProtoReflect() protoreflect.Message { func (x *CheckTask) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[17] mi := &file_controlplane_v1_controlplane_proto_msgTypes[18]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1748,7 +1826,7 @@ func (x *CheckTask) ProtoReflect() protoreflect.Message {
// Deprecated: Use CheckTask.ProtoReflect.Descriptor instead. // Deprecated: Use CheckTask.ProtoReflect.Descriptor instead.
func (*CheckTask) Descriptor() ([]byte, []int) { func (*CheckTask) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{17} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{18}
} }
func (x *CheckTask) GetTaskId() string { func (x *CheckTask) GetTaskId() string {
@ -1845,7 +1923,7 @@ type ObservationBatch struct {
func (x *ObservationBatch) Reset() { func (x *ObservationBatch) Reset() {
*x = ObservationBatch{} *x = ObservationBatch{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[18] mi := &file_controlplane_v1_controlplane_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1857,7 +1935,7 @@ func (x *ObservationBatch) String() string {
func (*ObservationBatch) ProtoMessage() {} func (*ObservationBatch) ProtoMessage() {}
func (x *ObservationBatch) ProtoReflect() protoreflect.Message { func (x *ObservationBatch) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[18] mi := &file_controlplane_v1_controlplane_proto_msgTypes[19]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1870,7 +1948,7 @@ func (x *ObservationBatch) ProtoReflect() protoreflect.Message {
// Deprecated: Use ObservationBatch.ProtoReflect.Descriptor instead. // Deprecated: Use ObservationBatch.ProtoReflect.Descriptor instead.
func (*ObservationBatch) Descriptor() ([]byte, []int) { func (*ObservationBatch) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{18} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{19}
} }
func (x *ObservationBatch) GetCheckerId() string { func (x *ObservationBatch) GetCheckerId() string {
@ -1905,7 +1983,7 @@ type HealthObservation struct {
func (x *HealthObservation) Reset() { func (x *HealthObservation) Reset() {
*x = HealthObservation{} *x = HealthObservation{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[19] mi := &file_controlplane_v1_controlplane_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1917,7 +1995,7 @@ func (x *HealthObservation) String() string {
func (*HealthObservation) ProtoMessage() {} func (*HealthObservation) ProtoMessage() {}
func (x *HealthObservation) ProtoReflect() protoreflect.Message { func (x *HealthObservation) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[19] mi := &file_controlplane_v1_controlplane_proto_msgTypes[20]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1930,7 +2008,7 @@ func (x *HealthObservation) ProtoReflect() protoreflect.Message {
// Deprecated: Use HealthObservation.ProtoReflect.Descriptor instead. // Deprecated: Use HealthObservation.ProtoReflect.Descriptor instead.
func (*HealthObservation) Descriptor() ([]byte, []int) { func (*HealthObservation) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{19} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{20}
} }
func (x *HealthObservation) GetTaskId() string { func (x *HealthObservation) GetTaskId() string {
@ -2013,7 +2091,7 @@ type ReportObservationsResponse struct {
func (x *ReportObservationsResponse) Reset() { func (x *ReportObservationsResponse) Reset() {
*x = ReportObservationsResponse{} *x = ReportObservationsResponse{}
mi := &file_controlplane_v1_controlplane_proto_msgTypes[20] mi := &file_controlplane_v1_controlplane_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -2025,7 +2103,7 @@ func (x *ReportObservationsResponse) String() string {
func (*ReportObservationsResponse) ProtoMessage() {} func (*ReportObservationsResponse) ProtoMessage() {}
func (x *ReportObservationsResponse) ProtoReflect() protoreflect.Message { func (x *ReportObservationsResponse) ProtoReflect() protoreflect.Message {
mi := &file_controlplane_v1_controlplane_proto_msgTypes[20] mi := &file_controlplane_v1_controlplane_proto_msgTypes[21]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -2038,7 +2116,7 @@ func (x *ReportObservationsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReportObservationsResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ReportObservationsResponse.ProtoReflect.Descriptor instead.
func (*ReportObservationsResponse) Descriptor() ([]byte, []int) { func (*ReportObservationsResponse) Descriptor() ([]byte, []int) {
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{20} return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{21}
} }
func (x *ReportObservationsResponse) GetAccepted() uint32 { func (x *ReportObservationsResponse) GetAccepted() uint32 {
@ -2086,7 +2164,7 @@ const file_controlplane_v1_controlplane_proto_rawDesc = "" +
"\x10SnapshotEnvelope\x12@\n" + "\x10SnapshotEnvelope\x12@\n" +
"\x04full\x18\x01 \x01(\v2*.proxy_pool.controlplane.v1.WorkerSnapshotH\x00R\x04full\x12A\n" + "\x04full\x18\x01 \x01(\v2*.proxy_pool.controlplane.v1.WorkerSnapshotH\x00R\x04full\x12A\n" +
"\x05delta\x18\x02 \x01(\v2).proxy_pool.controlplane.v1.SnapshotDeltaH\x00R\x05deltaB\t\n" + "\x05delta\x18\x02 \x01(\v2).proxy_pool.controlplane.v1.SnapshotDeltaH\x00R\x05deltaB\t\n" +
"\apayload\"\xf0\x02\n" + "\apayload\"\xc2\x03\n" +
"\x0eWorkerSnapshot\x12\x18\n" + "\x0eWorkerSnapshot\x12\x18\n" +
"\aversion\x18\x01 \x01(\x04R\aversion\x12'\n" + "\aversion\x18\x01 \x01(\x04R\aversion\x12'\n" +
"\x0fownership_epoch\x18\x02 \x01(\x04R\x0eownershipEpoch\x12=\n" + "\x0fownership_epoch\x18\x02 \x01(\x04R\x0eownershipEpoch\x12=\n" +
@ -2095,7 +2173,14 @@ const file_controlplane_v1_controlplane_proto_rawDesc = "" +
"validUntil\x12\x1a\n" + "validUntil\x12\x1a\n" +
"\bchecksum\x18\x05 \x01(\fR\bchecksum\x12A\n" + "\bchecksum\x18\x05 \x01(\fR\bchecksum\x12A\n" +
"\arouting\x18\x06 \x03(\v2'.proxy_pool.controlplane.v1.RoutingRuleR\arouting\x12@\n" + "\arouting\x18\x06 \x03(\v2'.proxy_pool.controlplane.v1.RoutingRuleR\arouting\x12@\n" +
"\aproxies\x18\a \x03(\v2&.proxy_pool.controlplane.v1.OwnedProxyR\aproxies\"\xd7\x03\n" + "\aproxies\x18\a \x03(\v2&.proxy_pool.controlplane.v1.OwnedProxyR\aproxies\x12P\n" +
"\vcredentials\x18\x0e \x03(\v2..proxy_pool.controlplane.v1.SnapshotCredentialR\vcredentials\"\x9a\x01\n" +
"\x12SnapshotCredential\x12\x1d\n" +
"\n" +
"secret_ref\x18\x01 \x01(\tR\tsecretRef\x12-\n" +
"\x12credential_version\x18\x02 \x01(\tR\x11credentialVersion\x12\x1a\n" +
"\busername\x18\x03 \x01(\tR\busername\x12\x1a\n" +
"\bpassword\x18\x04 \x01(\tR\bpassword\"\xd7\x03\n" +
"\rSnapshotDelta\x12!\n" + "\rSnapshotDelta\x12!\n" +
"\fbase_version\x18\x01 \x01(\x04R\vbaseVersion\x12\x18\n" + "\fbase_version\x18\x01 \x01(\x04R\vbaseVersion\x12\x18\n" +
"\aversion\x18\x02 \x01(\x04R\aversion\x12'\n" + "\aversion\x18\x02 \x01(\x04R\aversion\x12'\n" +
@ -2295,7 +2380,7 @@ func file_controlplane_v1_controlplane_proto_rawDescGZIP() []byte {
} }
var file_controlplane_v1_controlplane_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_controlplane_v1_controlplane_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
var file_controlplane_v1_controlplane_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_controlplane_v1_controlplane_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
var file_controlplane_v1_controlplane_proto_goTypes = []any{ var file_controlplane_v1_controlplane_proto_goTypes = []any{
(StrategyType)(0), // 0: proxy_pool.controlplane.v1.StrategyType (StrategyType)(0), // 0: proxy_pool.controlplane.v1.StrategyType
(UnavailableAction)(0), // 1: proxy_pool.controlplane.v1.UnavailableAction (UnavailableAction)(0), // 1: proxy_pool.controlplane.v1.UnavailableAction
@ -2307,87 +2392,89 @@ var file_controlplane_v1_controlplane_proto_goTypes = []any{
(*WatchSnapshotsRequest)(nil), // 7: proxy_pool.controlplane.v1.WatchSnapshotsRequest (*WatchSnapshotsRequest)(nil), // 7: proxy_pool.controlplane.v1.WatchSnapshotsRequest
(*SnapshotEnvelope)(nil), // 8: proxy_pool.controlplane.v1.SnapshotEnvelope (*SnapshotEnvelope)(nil), // 8: proxy_pool.controlplane.v1.SnapshotEnvelope
(*WorkerSnapshot)(nil), // 9: proxy_pool.controlplane.v1.WorkerSnapshot (*WorkerSnapshot)(nil), // 9: proxy_pool.controlplane.v1.WorkerSnapshot
(*SnapshotDelta)(nil), // 10: proxy_pool.controlplane.v1.SnapshotDelta (*SnapshotCredential)(nil), // 10: proxy_pool.controlplane.v1.SnapshotCredential
(*RoutingRule)(nil), // 11: proxy_pool.controlplane.v1.RoutingRule (*SnapshotDelta)(nil), // 11: proxy_pool.controlplane.v1.SnapshotDelta
(*RoutingStrategy)(nil), // 12: proxy_pool.controlplane.v1.RoutingStrategy (*RoutingRule)(nil), // 12: proxy_pool.controlplane.v1.RoutingRule
(*OwnedProxy)(nil), // 13: proxy_pool.controlplane.v1.OwnedProxy (*RoutingStrategy)(nil), // 13: proxy_pool.controlplane.v1.RoutingStrategy
(*AcknowledgeSnapshotRequest)(nil), // 14: proxy_pool.controlplane.v1.AcknowledgeSnapshotRequest (*OwnedProxy)(nil), // 14: proxy_pool.controlplane.v1.OwnedProxy
(*OutcomeBatch)(nil), // 15: proxy_pool.controlplane.v1.OutcomeBatch (*AcknowledgeSnapshotRequest)(nil), // 15: proxy_pool.controlplane.v1.AcknowledgeSnapshotRequest
(*ProxyOutcome)(nil), // 16: proxy_pool.controlplane.v1.ProxyOutcome (*OutcomeBatch)(nil), // 16: proxy_pool.controlplane.v1.OutcomeBatch
(*ReportOutcomesResponse)(nil), // 17: proxy_pool.controlplane.v1.ReportOutcomesResponse (*ProxyOutcome)(nil), // 17: proxy_pool.controlplane.v1.ProxyOutcome
(*ReportRuntimeRequest)(nil), // 18: proxy_pool.controlplane.v1.ReportRuntimeRequest (*ReportOutcomesResponse)(nil), // 18: proxy_pool.controlplane.v1.ReportOutcomesResponse
(*ProxyRuntime)(nil), // 19: proxy_pool.controlplane.v1.ProxyRuntime (*ReportRuntimeRequest)(nil), // 19: proxy_pool.controlplane.v1.ReportRuntimeRequest
(*ReportRuntimeResponse)(nil), // 20: proxy_pool.controlplane.v1.ReportRuntimeResponse (*ProxyRuntime)(nil), // 20: proxy_pool.controlplane.v1.ProxyRuntime
(*StreamCheckTasksRequest)(nil), // 21: proxy_pool.controlplane.v1.StreamCheckTasksRequest (*ReportRuntimeResponse)(nil), // 21: proxy_pool.controlplane.v1.ReportRuntimeResponse
(*CheckTask)(nil), // 22: proxy_pool.controlplane.v1.CheckTask (*StreamCheckTasksRequest)(nil), // 22: proxy_pool.controlplane.v1.StreamCheckTasksRequest
(*ObservationBatch)(nil), // 23: proxy_pool.controlplane.v1.ObservationBatch (*CheckTask)(nil), // 23: proxy_pool.controlplane.v1.CheckTask
(*HealthObservation)(nil), // 24: proxy_pool.controlplane.v1.HealthObservation (*ObservationBatch)(nil), // 24: proxy_pool.controlplane.v1.ObservationBatch
(*ReportObservationsResponse)(nil), // 25: proxy_pool.controlplane.v1.ReportObservationsResponse (*HealthObservation)(nil), // 25: proxy_pool.controlplane.v1.HealthObservation
nil, // 26: proxy_pool.controlplane.v1.RegisterWorkerRequest.LabelsEntry (*ReportObservationsResponse)(nil), // 26: proxy_pool.controlplane.v1.ReportObservationsResponse
nil, // 27: proxy_pool.controlplane.v1.RoutingRule.HeadersEntry nil, // 27: proxy_pool.controlplane.v1.RegisterWorkerRequest.LabelsEntry
nil, // 28: proxy_pool.controlplane.v1.RoutingStrategy.WeightsEntry nil, // 28: proxy_pool.controlplane.v1.RoutingRule.HeadersEntry
nil, // 29: proxy_pool.controlplane.v1.OwnedProxy.TagsEntry nil, // 29: proxy_pool.controlplane.v1.RoutingStrategy.WeightsEntry
(*durationpb.Duration)(nil), // 30: google.protobuf.Duration nil, // 30: proxy_pool.controlplane.v1.OwnedProxy.TagsEntry
(*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp (*durationpb.Duration)(nil), // 31: google.protobuf.Duration
(*emptypb.Empty)(nil), // 32: google.protobuf.Empty (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 33: google.protobuf.Empty
} }
var file_controlplane_v1_controlplane_proto_depIdxs = []int32{ var file_controlplane_v1_controlplane_proto_depIdxs = []int32{
26, // 0: proxy_pool.controlplane.v1.RegisterWorkerRequest.labels:type_name -> proxy_pool.controlplane.v1.RegisterWorkerRequest.LabelsEntry 27, // 0: proxy_pool.controlplane.v1.RegisterWorkerRequest.labels:type_name -> proxy_pool.controlplane.v1.RegisterWorkerRequest.LabelsEntry
30, // 1: proxy_pool.controlplane.v1.RegisterWorkerResponse.heartbeat_interval:type_name -> google.protobuf.Duration 31, // 1: proxy_pool.controlplane.v1.RegisterWorkerResponse.heartbeat_interval:type_name -> google.protobuf.Duration
30, // 2: proxy_pool.controlplane.v1.RegisterWorkerResponse.max_stale_age:type_name -> google.protobuf.Duration 31, // 2: proxy_pool.controlplane.v1.RegisterWorkerResponse.max_stale_age:type_name -> google.protobuf.Duration
9, // 3: proxy_pool.controlplane.v1.SnapshotEnvelope.full:type_name -> proxy_pool.controlplane.v1.WorkerSnapshot 9, // 3: proxy_pool.controlplane.v1.SnapshotEnvelope.full:type_name -> proxy_pool.controlplane.v1.WorkerSnapshot
10, // 4: proxy_pool.controlplane.v1.SnapshotEnvelope.delta:type_name -> proxy_pool.controlplane.v1.SnapshotDelta 11, // 4: proxy_pool.controlplane.v1.SnapshotEnvelope.delta:type_name -> proxy_pool.controlplane.v1.SnapshotDelta
31, // 5: proxy_pool.controlplane.v1.WorkerSnapshot.generated_at:type_name -> google.protobuf.Timestamp 32, // 5: proxy_pool.controlplane.v1.WorkerSnapshot.generated_at:type_name -> google.protobuf.Timestamp
31, // 6: proxy_pool.controlplane.v1.WorkerSnapshot.valid_until:type_name -> google.protobuf.Timestamp 32, // 6: proxy_pool.controlplane.v1.WorkerSnapshot.valid_until:type_name -> google.protobuf.Timestamp
11, // 7: proxy_pool.controlplane.v1.WorkerSnapshot.routing:type_name -> proxy_pool.controlplane.v1.RoutingRule 12, // 7: proxy_pool.controlplane.v1.WorkerSnapshot.routing:type_name -> proxy_pool.controlplane.v1.RoutingRule
13, // 8: proxy_pool.controlplane.v1.WorkerSnapshot.proxies:type_name -> proxy_pool.controlplane.v1.OwnedProxy 14, // 8: proxy_pool.controlplane.v1.WorkerSnapshot.proxies:type_name -> proxy_pool.controlplane.v1.OwnedProxy
31, // 9: proxy_pool.controlplane.v1.SnapshotDelta.generated_at:type_name -> google.protobuf.Timestamp 10, // 9: proxy_pool.controlplane.v1.WorkerSnapshot.credentials:type_name -> proxy_pool.controlplane.v1.SnapshotCredential
11, // 10: proxy_pool.controlplane.v1.SnapshotDelta.upserted_routing:type_name -> proxy_pool.controlplane.v1.RoutingRule 32, // 10: proxy_pool.controlplane.v1.SnapshotDelta.generated_at:type_name -> google.protobuf.Timestamp
13, // 11: proxy_pool.controlplane.v1.SnapshotDelta.upserted_proxies:type_name -> proxy_pool.controlplane.v1.OwnedProxy 12, // 11: proxy_pool.controlplane.v1.SnapshotDelta.upserted_routing:type_name -> proxy_pool.controlplane.v1.RoutingRule
27, // 12: proxy_pool.controlplane.v1.RoutingRule.headers:type_name -> proxy_pool.controlplane.v1.RoutingRule.HeadersEntry 14, // 12: proxy_pool.controlplane.v1.SnapshotDelta.upserted_proxies:type_name -> proxy_pool.controlplane.v1.OwnedProxy
12, // 13: proxy_pool.controlplane.v1.RoutingRule.strategy:type_name -> proxy_pool.controlplane.v1.RoutingStrategy 28, // 13: proxy_pool.controlplane.v1.RoutingRule.headers:type_name -> proxy_pool.controlplane.v1.RoutingRule.HeadersEntry
1, // 14: proxy_pool.controlplane.v1.RoutingRule.on_unavailable:type_name -> proxy_pool.controlplane.v1.UnavailableAction 13, // 14: proxy_pool.controlplane.v1.RoutingRule.strategy:type_name -> proxy_pool.controlplane.v1.RoutingStrategy
30, // 15: proxy_pool.controlplane.v1.RoutingRule.wait_timeout:type_name -> google.protobuf.Duration 1, // 15: proxy_pool.controlplane.v1.RoutingRule.on_unavailable:type_name -> proxy_pool.controlplane.v1.UnavailableAction
0, // 16: proxy_pool.controlplane.v1.RoutingStrategy.type:type_name -> proxy_pool.controlplane.v1.StrategyType 31, // 16: proxy_pool.controlplane.v1.RoutingRule.wait_timeout:type_name -> google.protobuf.Duration
28, // 17: proxy_pool.controlplane.v1.RoutingStrategy.weights:type_name -> proxy_pool.controlplane.v1.RoutingStrategy.WeightsEntry 0, // 17: proxy_pool.controlplane.v1.RoutingStrategy.type:type_name -> proxy_pool.controlplane.v1.StrategyType
2, // 18: proxy_pool.controlplane.v1.OwnedProxy.protocol:type_name -> proxy_pool.controlplane.v1.ProxyProtocol 29, // 18: proxy_pool.controlplane.v1.RoutingStrategy.weights:type_name -> proxy_pool.controlplane.v1.RoutingStrategy.WeightsEntry
31, // 19: proxy_pool.controlplane.v1.OwnedProxy.expires_at:type_name -> google.protobuf.Timestamp 2, // 19: proxy_pool.controlplane.v1.OwnedProxy.protocol:type_name -> proxy_pool.controlplane.v1.ProxyProtocol
29, // 20: proxy_pool.controlplane.v1.OwnedProxy.tags:type_name -> proxy_pool.controlplane.v1.OwnedProxy.TagsEntry 32, // 20: proxy_pool.controlplane.v1.OwnedProxy.expires_at:type_name -> google.protobuf.Timestamp
31, // 21: proxy_pool.controlplane.v1.OwnedProxy.usable_until:type_name -> google.protobuf.Timestamp 30, // 21: proxy_pool.controlplane.v1.OwnedProxy.tags:type_name -> proxy_pool.controlplane.v1.OwnedProxy.TagsEntry
16, // 22: proxy_pool.controlplane.v1.OutcomeBatch.outcomes:type_name -> proxy_pool.controlplane.v1.ProxyOutcome 32, // 22: proxy_pool.controlplane.v1.OwnedProxy.usable_until:type_name -> google.protobuf.Timestamp
3, // 23: proxy_pool.controlplane.v1.ProxyOutcome.stage:type_name -> proxy_pool.controlplane.v1.OutcomeStage 17, // 23: proxy_pool.controlplane.v1.OutcomeBatch.outcomes:type_name -> proxy_pool.controlplane.v1.ProxyOutcome
30, // 24: proxy_pool.controlplane.v1.ProxyOutcome.latency:type_name -> google.protobuf.Duration 3, // 24: proxy_pool.controlplane.v1.ProxyOutcome.stage:type_name -> proxy_pool.controlplane.v1.OutcomeStage
31, // 25: proxy_pool.controlplane.v1.ProxyOutcome.observed_at:type_name -> google.protobuf.Timestamp 31, // 25: proxy_pool.controlplane.v1.ProxyOutcome.latency:type_name -> google.protobuf.Duration
19, // 26: proxy_pool.controlplane.v1.ReportRuntimeRequest.counters:type_name -> proxy_pool.controlplane.v1.ProxyRuntime 32, // 26: proxy_pool.controlplane.v1.ProxyOutcome.observed_at:type_name -> google.protobuf.Timestamp
31, // 27: proxy_pool.controlplane.v1.ReportRuntimeRequest.observed_at:type_name -> google.protobuf.Timestamp 20, // 27: proxy_pool.controlplane.v1.ReportRuntimeRequest.counters:type_name -> proxy_pool.controlplane.v1.ProxyRuntime
4, // 28: proxy_pool.controlplane.v1.StreamCheckTasksRequest.supported_levels:type_name -> proxy_pool.controlplane.v1.CheckLevel 32, // 28: proxy_pool.controlplane.v1.ReportRuntimeRequest.observed_at:type_name -> google.protobuf.Timestamp
2, // 29: proxy_pool.controlplane.v1.CheckTask.protocol:type_name -> proxy_pool.controlplane.v1.ProxyProtocol 4, // 29: proxy_pool.controlplane.v1.StreamCheckTasksRequest.supported_levels:type_name -> proxy_pool.controlplane.v1.CheckLevel
4, // 30: proxy_pool.controlplane.v1.CheckTask.level:type_name -> proxy_pool.controlplane.v1.CheckLevel 2, // 30: proxy_pool.controlplane.v1.CheckTask.protocol:type_name -> proxy_pool.controlplane.v1.ProxyProtocol
30, // 31: proxy_pool.controlplane.v1.CheckTask.timeout:type_name -> google.protobuf.Duration 4, // 31: proxy_pool.controlplane.v1.CheckTask.level:type_name -> proxy_pool.controlplane.v1.CheckLevel
31, // 32: proxy_pool.controlplane.v1.CheckTask.deadline:type_name -> google.protobuf.Timestamp 31, // 32: proxy_pool.controlplane.v1.CheckTask.timeout:type_name -> google.protobuf.Duration
24, // 33: proxy_pool.controlplane.v1.ObservationBatch.observations:type_name -> proxy_pool.controlplane.v1.HealthObservation 32, // 33: proxy_pool.controlplane.v1.CheckTask.deadline:type_name -> google.protobuf.Timestamp
4, // 34: proxy_pool.controlplane.v1.HealthObservation.level:type_name -> proxy_pool.controlplane.v1.CheckLevel 25, // 34: proxy_pool.controlplane.v1.ObservationBatch.observations:type_name -> proxy_pool.controlplane.v1.HealthObservation
30, // 35: proxy_pool.controlplane.v1.HealthObservation.latency:type_name -> google.protobuf.Duration 4, // 35: proxy_pool.controlplane.v1.HealthObservation.level:type_name -> proxy_pool.controlplane.v1.CheckLevel
31, // 36: proxy_pool.controlplane.v1.HealthObservation.observed_at:type_name -> google.protobuf.Timestamp 31, // 36: proxy_pool.controlplane.v1.HealthObservation.latency:type_name -> google.protobuf.Duration
5, // 37: proxy_pool.controlplane.v1.WorkerControlPlane.RegisterWorker:input_type -> proxy_pool.controlplane.v1.RegisterWorkerRequest 32, // 37: proxy_pool.controlplane.v1.HealthObservation.observed_at:type_name -> google.protobuf.Timestamp
7, // 38: proxy_pool.controlplane.v1.WorkerControlPlane.WatchSnapshots:input_type -> proxy_pool.controlplane.v1.WatchSnapshotsRequest 5, // 38: proxy_pool.controlplane.v1.WorkerControlPlane.RegisterWorker:input_type -> proxy_pool.controlplane.v1.RegisterWorkerRequest
14, // 39: proxy_pool.controlplane.v1.WorkerControlPlane.AcknowledgeSnapshot:input_type -> proxy_pool.controlplane.v1.AcknowledgeSnapshotRequest 7, // 39: proxy_pool.controlplane.v1.WorkerControlPlane.WatchSnapshots:input_type -> proxy_pool.controlplane.v1.WatchSnapshotsRequest
15, // 40: proxy_pool.controlplane.v1.WorkerControlPlane.ReportOutcomes:input_type -> proxy_pool.controlplane.v1.OutcomeBatch 15, // 40: proxy_pool.controlplane.v1.WorkerControlPlane.AcknowledgeSnapshot:input_type -> proxy_pool.controlplane.v1.AcknowledgeSnapshotRequest
18, // 41: proxy_pool.controlplane.v1.WorkerControlPlane.ReportRuntime:input_type -> proxy_pool.controlplane.v1.ReportRuntimeRequest 16, // 41: proxy_pool.controlplane.v1.WorkerControlPlane.ReportOutcomes:input_type -> proxy_pool.controlplane.v1.OutcomeBatch
21, // 42: proxy_pool.controlplane.v1.CheckerControlPlane.StreamCheckTasks:input_type -> proxy_pool.controlplane.v1.StreamCheckTasksRequest 19, // 42: proxy_pool.controlplane.v1.WorkerControlPlane.ReportRuntime:input_type -> proxy_pool.controlplane.v1.ReportRuntimeRequest
23, // 43: proxy_pool.controlplane.v1.CheckerControlPlane.ReportObservations:input_type -> proxy_pool.controlplane.v1.ObservationBatch 22, // 43: proxy_pool.controlplane.v1.CheckerControlPlane.StreamCheckTasks:input_type -> proxy_pool.controlplane.v1.StreamCheckTasksRequest
6, // 44: proxy_pool.controlplane.v1.WorkerControlPlane.RegisterWorker:output_type -> proxy_pool.controlplane.v1.RegisterWorkerResponse 24, // 44: proxy_pool.controlplane.v1.CheckerControlPlane.ReportObservations:input_type -> proxy_pool.controlplane.v1.ObservationBatch
8, // 45: proxy_pool.controlplane.v1.WorkerControlPlane.WatchSnapshots:output_type -> proxy_pool.controlplane.v1.SnapshotEnvelope 6, // 45: proxy_pool.controlplane.v1.WorkerControlPlane.RegisterWorker:output_type -> proxy_pool.controlplane.v1.RegisterWorkerResponse
32, // 46: proxy_pool.controlplane.v1.WorkerControlPlane.AcknowledgeSnapshot:output_type -> google.protobuf.Empty 8, // 46: proxy_pool.controlplane.v1.WorkerControlPlane.WatchSnapshots:output_type -> proxy_pool.controlplane.v1.SnapshotEnvelope
17, // 47: proxy_pool.controlplane.v1.WorkerControlPlane.ReportOutcomes:output_type -> proxy_pool.controlplane.v1.ReportOutcomesResponse 33, // 47: proxy_pool.controlplane.v1.WorkerControlPlane.AcknowledgeSnapshot:output_type -> google.protobuf.Empty
20, // 48: proxy_pool.controlplane.v1.WorkerControlPlane.ReportRuntime:output_type -> proxy_pool.controlplane.v1.ReportRuntimeResponse 18, // 48: proxy_pool.controlplane.v1.WorkerControlPlane.ReportOutcomes:output_type -> proxy_pool.controlplane.v1.ReportOutcomesResponse
22, // 49: proxy_pool.controlplane.v1.CheckerControlPlane.StreamCheckTasks:output_type -> proxy_pool.controlplane.v1.CheckTask 21, // 49: proxy_pool.controlplane.v1.WorkerControlPlane.ReportRuntime:output_type -> proxy_pool.controlplane.v1.ReportRuntimeResponse
25, // 50: proxy_pool.controlplane.v1.CheckerControlPlane.ReportObservations:output_type -> proxy_pool.controlplane.v1.ReportObservationsResponse 23, // 50: proxy_pool.controlplane.v1.CheckerControlPlane.StreamCheckTasks:output_type -> proxy_pool.controlplane.v1.CheckTask
44, // [44:51] is the sub-list for method output_type 26, // 51: proxy_pool.controlplane.v1.CheckerControlPlane.ReportObservations:output_type -> proxy_pool.controlplane.v1.ReportObservationsResponse
37, // [37:44] is the sub-list for method input_type 45, // [45:52] is the sub-list for method output_type
37, // [37:37] is the sub-list for extension type_name 38, // [38:45] is the sub-list for method input_type
37, // [37:37] is the sub-list for extension extendee 38, // [38:38] is the sub-list for extension type_name
0, // [0:37] is the sub-list for field type_name 38, // [38:38] is the sub-list for extension extendee
0, // [0:38] is the sub-list for field type_name
} }
func init() { file_controlplane_v1_controlplane_proto_init() } func init() { file_controlplane_v1_controlplane_proto_init() }
@ -2405,7 +2492,7 @@ func file_controlplane_v1_controlplane_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_controlplane_v1_controlplane_proto_rawDesc), len(file_controlplane_v1_controlplane_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controlplane_v1_controlplane_proto_rawDesc), len(file_controlplane_v1_controlplane_proto_rawDesc)),
NumEnums: 5, NumEnums: 5,
NumMessages: 25, NumMessages: 26,
NumExtensions: 0, NumExtensions: 0,
NumServices: 2, NumServices: 2,
}, },

View File

@ -139,6 +139,7 @@ type ControlPlane struct {
MaxRuntimeCounters int `yaml:"maxRuntimeCounters"` MaxRuntimeCounters int `yaml:"maxRuntimeCounters"`
MaxConcurrentStreams uint32 `yaml:"maxConcurrentStreams"` MaxConcurrentStreams uint32 `yaml:"maxConcurrentStreams"`
TLS ControlPlaneTLS `yaml:"tls"` TLS ControlPlaneTLS `yaml:"tls"`
GatewayTLS GatewayTLS `yaml:"gatewayTLS"`
} }
type ControlPlaneTLS struct { type ControlPlaneTLS struct {
@ -150,6 +151,14 @@ type ControlPlaneTLS struct {
Environment string `yaml:"environment"` Environment string `yaml:"environment"`
} }
// GatewayTLS holds client-only mTLS material. It remains separate from the
// Controller's server certificate and CA configuration.
type GatewayTLS struct {
CertFile string `yaml:"certFile"`
KeyFile string `yaml:"keyFile"`
ServerCAFile string `yaml:"serverCAFile"`
}
type Storage struct { type Storage struct {
PostgresURL string `yaml:"postgresURL"` PostgresURL string `yaml:"postgresURL"`
RedisURL string `yaml:"redisURL"` RedisURL string `yaml:"redisURL"`

View File

@ -454,6 +454,14 @@ func TestValidateControlPlane(t *testing.T) {
}, },
want: "environment", want: "environment",
}, },
{
name: "partial gateway tls",
mutate: func(cfg *Config) {
cfg.ControlPlane = validMTLSControlPlane()
cfg.ControlPlane.GatewayTLS.CertFile = "/run/secrets/gateway-cert.pem"
},
want: "gatewayTLS",
},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -110,6 +110,9 @@ func Validate(cfg *Config) error {
} }
func validateControlPlane(item ControlPlane) error { func validateControlPlane(item ControlPlane) error {
if err := validateGatewayTLS(item.GatewayTLS); err != nil {
return err
}
if !item.Enabled { if !item.Enabled {
return nil return nil
} }
@ -161,6 +164,17 @@ func validateControlPlane(item ControlPlane) error {
return nil return nil
} }
func validateGatewayTLS(item GatewayTLS) error {
configured := item.CertFile != "" || item.KeyFile != "" || item.ServerCAFile != ""
if !configured {
return nil
}
if item.CertFile == "" || item.KeyFile == "" || item.ServerCAFile == "" {
return fmt.Errorf("validate controlPlane gatewayTLS: certFile, keyFile and serverCAFile are required together")
}
return nil
}
func validTrustDomain(value string) bool { func validTrustDomain(value string) bool {
if len(value) == 0 || len(value) > 253 { if len(value) == 0 || len(value) > 253 {
return false return false

View File

@ -257,6 +257,7 @@ func runWithWorkerFactory(
MaxSnapshotBytes: loaded.Value.ControlPlane.MaxMessageBytes, MaxSnapshotBytes: loaded.Value.ControlPlane.MaxMessageBytes,
SnapshotReader: snapshotReader, SnapshotReader: snapshotReader,
RoutingSource: routingSource, RoutingSource: routingSource,
Credentials: opened.credentials,
}) })
if serviceErr != nil { if serviceErr != nil {
return fmt.Errorf("%w: build Worker control service: %w", ErrStartup, serviceErr) return fmt.Errorf("%w: build Worker control service: %w", ErrStartup, serviceErr)

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"math" "math"
"sort"
"time" "time"
controlplanev1 "proxy-pool/gen/controlplane/v1" controlplanev1 "proxy-pool/gen/controlplane/v1"
@ -11,6 +12,7 @@ import (
ownershipDomain "proxy-pool/internal/domain/ownership" ownershipDomain "proxy-pool/internal/domain/ownership"
proxyDomain "proxy-pool/internal/domain/proxy" proxyDomain "proxy-pool/internal/domain/proxy"
"proxy-pool/internal/domain/workerruntime" "proxy-pool/internal/domain/workerruntime"
platformCredentials "proxy-pool/internal/platform/credentials"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
@ -21,13 +23,18 @@ var ErrSnapshotCredentialsUnavailable = errors.New("worker snapshot credential m
// OwnedSnapshotSource builds a complete Worker view from the Redis ownership // OwnedSnapshotSource builds a complete Worker view from the Redis ownership
// index. It only accepts a complete bounded result from the reader. // index. It only accepts a complete bounded result from the reader.
type OwnedSnapshotSource struct { type OwnedSnapshotSource struct {
epochs OwnershipEpochReader epochs OwnershipEpochReader
reader ownershipDomain.SnapshotReader reader ownershipDomain.SnapshotReader
validFor time.Duration validFor time.Duration
maxProxies int maxProxies int
maxBytes int maxBytes int
now func() time.Time now func() time.Time
routing RoutingSource routing RoutingSource
credentials CredentialReader
}
type CredentialReader interface {
Resolve(context.Context, platformCredentials.Reference) (platformCredentials.Value, error)
} }
func NewOwnedSnapshotSource( func NewOwnedSnapshotSource(
@ -38,6 +45,35 @@ func NewOwnedSnapshotSource(
maxBytes int, maxBytes int,
now func() time.Time, now func() time.Time,
routing ...RoutingSource, routing ...RoutingSource,
) (*OwnedSnapshotSource, error) {
return newOwnedSnapshotSource(epochs, reader, validFor, maxProxies, maxBytes, now, nil, routing...)
}
func NewOwnedSnapshotSourceWithCredentials(
epochs OwnershipEpochReader,
reader ownershipDomain.SnapshotReader,
validFor time.Duration,
maxProxies int,
maxBytes int,
now func() time.Time,
credentials CredentialReader,
routing ...RoutingSource,
) (*OwnedSnapshotSource, error) {
if credentials == nil {
return nil, ErrSnapshotsUnavailable
}
return newOwnedSnapshotSource(epochs, reader, validFor, maxProxies, maxBytes, now, credentials, routing...)
}
func newOwnedSnapshotSource(
epochs OwnershipEpochReader,
reader ownershipDomain.SnapshotReader,
validFor time.Duration,
maxProxies int,
maxBytes int,
now func() time.Time,
credentials CredentialReader,
routing ...RoutingSource,
) (*OwnedSnapshotSource, error) { ) (*OwnedSnapshotSource, error) {
if epochs == nil || reader == nil || validFor <= 0 || maxProxies <= 0 || maxBytes <= 0 || now == nil || len(routing) > 1 || if epochs == nil || reader == nil || validFor <= 0 || maxProxies <= 0 || maxBytes <= 0 || now == nil || len(routing) > 1 ||
len(routing) == 1 && routing[0] == nil { len(routing) == 1 && routing[0] == nil {
@ -45,6 +81,7 @@ func NewOwnedSnapshotSource(
} }
source := &OwnedSnapshotSource{ source := &OwnedSnapshotSource{
epochs: epochs, reader: reader, validFor: validFor, maxProxies: maxProxies, maxBytes: maxBytes, now: now, epochs: epochs, reader: reader, validFor: validFor, maxProxies: maxProxies, maxBytes: maxBytes, now: now,
credentials: credentials,
} }
if len(routing) == 1 { if len(routing) == 1 {
source.routing = routing[0] source.routing = routing[0]
@ -77,6 +114,7 @@ func (source *OwnedSnapshotSource) Watch(ctx context.Context, request SnapshotWa
GeneratedAt: timestamppb.New(now), ValidUntil: timestamppb.New(now.Add(source.validFor)), GeneratedAt: timestamppb.New(now), ValidUntil: timestamppb.New(now.Add(source.validFor)),
Proxies: make([]*controlplanev1.OwnedProxy, 0, len(proxies)), Proxies: make([]*controlplanev1.OwnedProxy, 0, len(proxies)),
} }
materials := make(map[platformCredentials.Reference]platformCredentials.Value)
for _, item := range proxies { for _, item := range proxies {
owned, validUntil, err := wireOwnedProxy(item, now) owned, validUntil, err := wireOwnedProxy(item, now)
if err != nil { if err != nil {
@ -86,7 +124,26 @@ func (source *OwnedSnapshotSource) Watch(ctx context.Context, request SnapshotWa
full.ValidUntil = timestamppb.New(validUntil) full.ValidUntil = timestamppb.New(validUntil)
} }
full.Proxies = append(full.Proxies, owned) full.Proxies = append(full.Proxies, owned)
if owned.GetSecretRef() != "" {
reference := platformCredentials.Reference{SecretRef: owned.GetSecretRef(), CredentialVersion: owned.GetCredentialVersion()}
if source.credentials == nil {
return nil, ErrSnapshotCredentialsUnavailable
}
material, resolveErr := source.credentials.Resolve(ctx, reference)
if resolveErr != nil || (material.Username == "" && material.Password == "") ||
(owned.GetUsername() != "" && material.Username != "" && material.Username != owned.GetUsername()) {
return nil, ErrSnapshotCredentialsUnavailable
}
if material.Username == "" {
material.Username = owned.GetUsername()
}
if existing, exists := materials[reference]; exists && existing != material {
return nil, ErrSnapshotCredentialsUnavailable
}
materials[reference] = material
}
} }
full.Credentials = wireSnapshotCredentials(materials)
if source.routing != nil { if source.routing != nil {
routing, err := source.routing.Read(ctx) routing, err := source.routing.Read(ctx)
if err != nil { if err != nil {
@ -107,6 +164,23 @@ func (source *OwnedSnapshotSource) Watch(ctx context.Context, request SnapshotWa
return updates, nil return updates, nil
} }
func wireSnapshotCredentials(materials map[platformCredentials.Reference]platformCredentials.Value) []*controlplanev1.SnapshotCredential {
result := make([]*controlplanev1.SnapshotCredential, 0, len(materials))
for reference, material := range materials {
result = append(result, &controlplanev1.SnapshotCredential{
SecretRef: reference.SecretRef, CredentialVersion: reference.CredentialVersion,
Username: material.Username, Password: material.Password,
})
}
sort.Slice(result, func(left, right int) bool {
if result[left].GetSecretRef() == result[right].GetSecretRef() {
return result[left].GetCredentialVersion() < result[right].GetCredentialVersion()
}
return result[left].GetSecretRef() < result[right].GetSecretRef()
})
return result
}
func cloneSnapshotRouting(source []*controlplanev1.RoutingRule) []*controlplanev1.RoutingRule { func cloneSnapshotRouting(source []*controlplanev1.RoutingRule) []*controlplanev1.RoutingRule {
result := make([]*controlplanev1.RoutingRule, len(source)) result := make([]*controlplanev1.RoutingRule, len(source))
for index, rule := range source { for index, rule := range source {
@ -119,7 +193,7 @@ func cloneSnapshotRouting(source []*controlplanev1.RoutingRule) []*controlplanev
func wireOwnedProxy(item ownershipDomain.SnapshotProxy, now time.Time) (*controlplanev1.OwnedProxy, time.Time, error) { func wireOwnedProxy(item ownershipDomain.SnapshotProxy, now time.Time) (*controlplanev1.OwnedProxy, time.Time, error) {
proxy := item.Proxy proxy := item.Proxy
if item.OwnershipEpoch == 0 || item.LeaseExpiresAt.IsZero() || proxy.CredentialVersion != "" || proxy.SecretRef != "" { if item.OwnershipEpoch == 0 || item.LeaseExpiresAt.IsZero() || (proxy.CredentialVersion == "") != (proxy.SecretRef == "") {
if proxy.CredentialVersion != "" || proxy.SecretRef != "" { if proxy.CredentialVersion != "" || proxy.SecretRef != "" {
return nil, time.Time{}, ErrSnapshotCredentialsUnavailable return nil, time.Time{}, ErrSnapshotCredentialsUnavailable
} }
@ -144,7 +218,7 @@ func wireOwnedProxy(item ownershipDomain.SnapshotProxy, now time.Time) (*control
} }
return &controlplanev1.OwnedProxy{ return &controlplanev1.OwnedProxy{
Id: proxy.ID, Upstream: proxy.SourceUpstream, Protocol: protocol, Host: proxy.Host, Port: uint32(proxy.Port), Id: proxy.ID, Upstream: proxy.SourceUpstream, Protocol: protocol, Host: proxy.Host, Port: uint32(proxy.Port),
Username: proxy.Username, ExpiresAt: timestamppb.New(proxy.ExpiresAt.UTC()), Username: proxy.Username, CredentialVersion: proxy.CredentialVersion, SecretRef: proxy.SecretRef, ExpiresAt: timestamppb.New(proxy.ExpiresAt.UTC()),
MaxConcurrency: uint32(proxy.MaxConcurrency), Tags: tags, OwnershipEpoch: item.OwnershipEpoch, MaxConcurrency: uint32(proxy.MaxConcurrency), Tags: tags, OwnershipEpoch: item.OwnershipEpoch,
UsableUntil: timestamppb.New(validUntil), UsableUntil: timestamppb.New(validUntil),
}, validUntil, nil }, validUntil, nil

View File

@ -10,6 +10,7 @@ import (
ownershipDomain "proxy-pool/internal/domain/ownership" ownershipDomain "proxy-pool/internal/domain/ownership"
"proxy-pool/internal/domain/workerruntime" "proxy-pool/internal/domain/workerruntime"
platformCredentials "proxy-pool/internal/platform/credentials"
) )
var ( var (
@ -59,6 +60,7 @@ type Options struct {
MaxSnapshotBytes int MaxSnapshotBytes int
SnapshotReader ownershipDomain.SnapshotReader SnapshotReader ownershipDomain.SnapshotReader
RoutingSource RoutingSource RoutingSource RoutingSource
Credentials platformCredentials.Store
SessionID func() (string, error) SessionID func() (string, error)
} }
@ -136,9 +138,17 @@ func NewService(store workerruntime.ControlStore, options Options) (Service, err
if options.RoutingSource != nil { if options.RoutingSource != nil {
routing = append(routing, options.RoutingSource) routing = append(routing, options.RoutingSource)
} }
source, err := NewOwnedSnapshotSource( var source SnapshotSource
result, options.SnapshotReader, options.MaxStaleAge, options.MaxRuntimeCounters, options.MaxSnapshotBytes, time.Now, routing..., var err error
) if options.Credentials != nil {
source, err = NewOwnedSnapshotSourceWithCredentials(
result, options.SnapshotReader, options.MaxStaleAge, options.MaxRuntimeCounters, options.MaxSnapshotBytes, time.Now, options.Credentials, routing...,
)
} else {
source, err = NewOwnedSnapshotSource(
result, options.SnapshotReader, options.MaxStaleAge, options.MaxRuntimeCounters, options.MaxSnapshotBytes, time.Now, routing...,
)
}
if err != nil { if err != nil {
return nil, errors.Join(ErrInvalidCommand, err) return nil, errors.Join(ErrInvalidCommand, err)
} }

View File

@ -10,6 +10,7 @@ import (
"proxy-pool/internal/controlplane/snapshotwire" "proxy-pool/internal/controlplane/snapshotwire"
ownershipDomain "proxy-pool/internal/domain/ownership" ownershipDomain "proxy-pool/internal/domain/ownership"
proxyDomain "proxy-pool/internal/domain/proxy" proxyDomain "proxy-pool/internal/domain/proxy"
platformCredentials "proxy-pool/internal/platform/credentials"
) )
func TestInitialSnapshotSourceIssuesNextFullSnapshot(t *testing.T) { func TestInitialSnapshotSourceIssuesNextFullSnapshot(t *testing.T) {
@ -123,6 +124,44 @@ func TestOwnedSnapshotSourceRejectsCredentialReferenceUntilMaterialIsAvailable(t
} }
} }
func TestOwnedSnapshotSourceIncludesReferencedCredentialMaterial(t *testing.T) {
now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
expiresAt := now.Add(time.Minute)
usableUntil := now.Add(30 * time.Second)
store, err := platformCredentials.NewMemoryStore(2)
if err != nil {
t.Fatalf("NewMemoryStore(): %v", err)
}
reference, err := store.Put(context.Background(), "provider-a", platformCredentials.Value{Username: "upstream", Password: "secret"})
if err != nil {
t.Fatalf("Put(): %v", err)
}
source, err := NewOwnedSnapshotSourceWithCredentials(epochReaderStub{epoch: 1}, snapshotReaderStub{proxies: []ownershipDomain.SnapshotProxy{{
Proxy: proxyDomain.Proxy{
ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080, SourceUpstream: "upstream-a",
Username: "upstream", SecretRef: reference.SecretRef, CredentialVersion: reference.CredentialVersion,
ExpiresAt: &expiresAt, UsableUntil: &usableUntil, MaxConcurrency: 1, State: proxyDomain.StateAvailable,
}, OwnershipEpoch: 1, LeaseExpiresAt: now.Add(time.Minute),
}}}, time.Minute, 10, 4096, func() time.Time { return now }, store)
if err != nil {
t.Fatalf("NewOwnedSnapshotSourceWithCredentials(): %v", err)
}
updates, err := source.Watch(context.Background(), SnapshotWatchRequest{WorkerID: "worker-a", SessionID: "session-a"})
if err != nil {
t.Fatalf("Watch(): %v", err)
}
full := <-updates
if len(full.GetCredentials()) != 1 || full.GetCredentials()[0].GetSecretRef() != reference.SecretRef ||
full.GetCredentials()[0].GetUsername() != "upstream" || full.GetCredentials()[0].GetPassword() != "secret" ||
full.GetProxies()[0].GetCredentialVersion() != reference.CredentialVersion {
t.Fatal("snapshot credential material or proxy credential reference is invalid")
}
checksum, err := snapshotwire.Checksum(full)
if err != nil || string(checksum[:]) != string(full.GetChecksum()) {
t.Fatalf("snapshot checksum = %x, %v; want %x", full.GetChecksum(), err, checksum)
}
}
type epochReaderStub struct { type epochReaderStub struct {
epoch uint64 epoch uint64
err error err error

View File

@ -0,0 +1,465 @@
// Package bootstrap assembles the proxy-gateway process from data-plane
// components without putting Controller or storage access on the request path.
package bootstrap
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
controlplanev1 "proxy-pool/gen/controlplane/v1"
"proxy-pool/internal/config"
proxyDomain "proxy-pool/internal/domain/proxy"
"proxy-pool/internal/domain/workerruntime"
"proxy-pool/internal/gateway/controlplane"
"proxy-pool/internal/gateway/dispatch"
"proxy-pool/internal/gateway/server"
"proxy-pool/internal/gateway/snapshot"
"proxy-pool/internal/gateway/transport"
"proxy-pool/internal/platform/httpserver"
"proxy-pool/internal/platform/lifecycle"
platformMetrics "proxy-pool/internal/platform/metrics"
)
var (
ErrInvalidOptions = errors.New("invalid gateway bootstrap options")
ErrStartup = errors.New("gateway startup failed")
ErrNotReady = errors.New("gateway snapshot is not ready")
)
const (
defaultReconnectInitialDelay = time.Second
defaultReconnectMaxDelay = 30 * time.Second
defaultReconnectJitter = 20
)
// Options provides process-local settings. ControlPlaneAddress is deliberately
// independent from controlPlane.listen: the latter is the Controller bind
// address, while this value is the Gateway's remote dial target.
type Options struct {
ConfigPath string
Resolver config.Resolver
ControlPlaneAddress string
ClusterID string
WorkerID string
InstanceID string
Zone string
Labels map[string]string
HTTP httpserver.Options
// The pre-bound listeners and transport are test seams. Production leaves
// them nil and derives listeners and mTLS credentials from configuration.
GatewayListener net.Listener
MetricsListener net.Listener
GRPCTransport credentials.TransportCredentials
ReconnectInitialDelay time.Duration
ReconnectMaxDelay time.Duration
ReconnectJitter int
}
func Run(ctx context.Context, options Options) error {
if err := validateOptions(ctx, options); err != nil {
return err
}
configuration, err := loadConfiguration(ctx, options.ConfigPath, options.Resolver)
if err != nil {
return fmt.Errorf("%w: load configuration: %w", ErrStartup, err)
}
if !configuration.Gateway.Enabled || !configuration.ControlPlane.Enabled {
return errors.Join(ErrInvalidOptions, errors.New("gateway and controlPlane must be enabled"))
}
runtime, err := newRuntime(ctx, configuration, options)
if err != nil {
return fmt.Errorf("%w: %w", ErrStartup, err)
}
defer runtime.Close()
return runtime.Run(ctx)
}
func validateOptions(ctx context.Context, options Options) error {
if ctx == nil || strings.TrimSpace(options.ConfigPath) != options.ConfigPath || options.ConfigPath == "" ||
nilInterface(options.Resolver) || !workerruntime.ValidIdentifier(options.ClusterID) || !workerruntime.ValidIdentifier(options.WorkerID) ||
!workerruntime.ValidIdentifier(options.InstanceID) || !workerruntime.ValidIdentifier(options.Zone) || !validDialAddress(options.ControlPlaneAddress) {
return ErrInvalidOptions
}
if options.GatewayListener == nil && options.MetricsListener != nil {
return ErrInvalidOptions
}
if options.ReconnectInitialDelay < 0 || options.ReconnectMaxDelay < 0 || options.ReconnectJitter < 0 || options.ReconnectJitter > 100 {
return ErrInvalidOptions
}
return nil
}
func loadConfiguration(ctx context.Context, path string, resolver config.Resolver) (*config.Config, error) {
if ctx == nil || strings.TrimSpace(path) != path || path == "" || nilInterface(resolver) {
return nil, ErrInvalidOptions
}
if err := ctx.Err(); err != nil {
return nil, err
}
content, err := resolver.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read configuration %q: %w", path, err)
}
if err := ctx.Err(); err != nil {
return nil, err
}
configuration, err := config.LoadResolved(bytes.NewReader(content), resolver)
if err != nil {
return nil, fmt.Errorf("decode configuration %q: %w", path, err)
}
return configuration, nil
}
type runtime struct {
connection *grpc.ClientConn
group *lifecycle.Group
}
func newRuntime(ctx context.Context, configuration *config.Config, options Options) (*runtime, error) {
if ctx == nil || configuration == nil {
return nil, ErrInvalidOptions
}
store := snapshot.NewStore(options.ClusterID, options.WorkerID)
protection, err := server.BuildProtection(configuration.Gateway)
if err != nil {
return nil, fmt.Errorf("build gateway protections: %w", err)
}
targets, err := server.TargetPolicyFromListener(configuration.Gateway)
if err != nil {
return nil, fmt.Errorf("build gateway target policy: %w", err)
}
proxyTransport := transport.New(transport.Config{}, snapshotCredentialResolver{store: store})
handler, err := server.New(server.ConfigFromListener(configuration.Gateway), server.Dependencies{
Auth: protection.Auth,
Access: protection.Access,
Admission: protection.Admission,
Targets: targets,
Router: server.NewSnapshotRouter(store),
Dispatcher: dispatch.New(store),
Transport: proxyTransport,
})
if err != nil {
proxyTransport.CloseIdleConnections()
return nil, fmt.Errorf("build gateway handler: %w", err)
}
transportCredentials, err := controlPlaneTransport(configuration.ControlPlane, options.ControlPlaneAddress, options.GRPCTransport)
if err != nil {
proxyTransport.CloseIdleConnections()
return nil, err
}
connection, err := grpc.NewClient(options.ControlPlaneAddress,
grpc.WithTransportCredentials(transportCredentials),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(configuration.ControlPlane.MaxMessageBytes),
grpc.MaxCallSendMsgSize(configuration.ControlPlane.MaxMessageBytes),
),
)
if err != nil {
proxyTransport.CloseIdleConnections()
return nil, fmt.Errorf("dial control plane: %w", err)
}
closeConnection := true
defer func() {
if closeConnection {
_ = connection.Close()
proxyTransport.CloseIdleConnections()
}
}()
client := controlplanev1.NewWorkerControlPlaneClient(connection)
reporter, err := controlplane.NewRuntimeReporter(generatedRuntimeClient{client: client}, store, controlplane.Options{
WorkerID: options.WorkerID, InstanceID: options.InstanceID, Zone: options.Zone,
ProtocolVersion: configuration.ControlPlane.ProtocolVersion, Labels: options.Labels, Now: time.Now,
})
if err != nil {
return nil, fmt.Errorf("build runtime reporter: %w", err)
}
watcher, err := controlplane.NewSnapshotWatcher(controlplane.NewGeneratedSnapshotRPCClient(client), store,
controlplane.SnapshotWatcherOptions{ClusterID: options.ClusterID, WorkerID: options.WorkerID})
if err != nil {
return nil, fmt.Errorf("build snapshot watcher: %w", err)
}
session, err := controlplane.NewSessionRunner(reporter, watcher)
if err != nil {
return nil, fmt.Errorf("build control plane session: %w", err)
}
reconnect, err := controlplane.NewSessionSupervisor(session, reconnectOptions(options))
if err != nil {
return nil, fmt.Errorf("build control plane reconnect supervisor: %w", err)
}
httpRuntime, err := newHTTPRuntime(ctx, configuration, options, handler, store)
if err != nil {
return nil, err
}
group, err := lifecycle.NewGroup(httpRuntime, reconnect)
if err != nil {
return nil, err
}
closeConnection = false
return &runtime{connection: connection, group: group}, nil
}
type generatedRuntimeClient struct {
client controlplanev1.WorkerControlPlaneClient
}
type snapshotCredentialResolver struct {
store *snapshot.Store
}
func (resolver snapshotCredentialResolver) Resolve(ctx context.Context, selected proxyDomain.Proxy) (transport.Credentials, error) {
credential, err := resolver.store.Credential(ctx, selected)
if err != nil {
return transport.Credentials{}, err
}
return transport.Credentials{Username: credential.Username, Password: credential.Password}, nil
}
func (client generatedRuntimeClient) RegisterWorker(
ctx context.Context,
request *controlplanev1.RegisterWorkerRequest,
) (*controlplanev1.RegisterWorkerResponse, error) {
return client.client.RegisterWorker(ctx, request)
}
func (client generatedRuntimeClient) ReportRuntime(
ctx context.Context,
request *controlplanev1.ReportRuntimeRequest,
) (*controlplanev1.ReportRuntimeResponse, error) {
return client.client.ReportRuntime(ctx, request)
}
func (runtime *runtime) Run(ctx context.Context) error {
if runtime == nil || runtime.group == nil || ctx == nil {
return ErrInvalidOptions
}
return runtime.group.Run(ctx)
}
func (runtime *runtime) Close() {
if runtime != nil && runtime.connection != nil {
_ = runtime.connection.Close()
}
}
func reconnectOptions(options Options) controlplane.ReconnectOptions {
initial := options.ReconnectInitialDelay
if initial == 0 {
initial = defaultReconnectInitialDelay
}
maximum := options.ReconnectMaxDelay
if maximum == 0 {
maximum = defaultReconnectMaxDelay
}
jitter := options.ReconnectJitter
if jitter == 0 {
jitter = defaultReconnectJitter
}
return controlplane.ReconnectOptions{InitialDelay: initial, MaxDelay: maximum, Jitter: jitter}
}
type httpRuntime struct {
options httpserver.Options
handler *server.Handler
endpoints []httpserver.Endpoint
}
func newHTTPRuntime(
ctx context.Context,
configuration *config.Config,
options Options,
handler *server.Handler,
store *snapshot.Store,
) (*httpRuntime, error) {
if ctx == nil || configuration == nil || handler == nil || store == nil {
return nil, ErrInvalidOptions
}
endpoints := make([]httpserver.Endpoint, 0, 2)
gatewayListener, gatewayOwned, err := resolveListener(ctx, configuration.Gateway.Listen, options.GatewayListener)
if err != nil {
return nil, fmt.Errorf("listen gateway: %w", err)
}
endpoints = append(endpoints, httpserver.Endpoint{Name: "gateway", Listener: gatewayListener, Handler: handler})
if configuration.Metrics.Enabled {
metricsHandler, metricsErr := platformMetrics.NewHandler(platformMetrics.Dependencies{
Gatherer: prometheus.DefaultGatherer,
Readiness: snapshotReadiness{store: store, now: time.Now},
})
if metricsErr != nil {
if gatewayOwned {
_ = gatewayListener.Close()
}
return nil, fmt.Errorf("build gateway metrics: %w", metricsErr)
}
metricsListener, metricsOwned, listenErr := resolveListener(ctx, configuration.Metrics.Listen, options.MetricsListener)
if listenErr != nil {
if gatewayOwned {
_ = gatewayListener.Close()
}
return nil, fmt.Errorf("listen gateway metrics: %w", listenErr)
}
_ = metricsOwned
endpoints = append(endpoints, httpserver.Endpoint{Name: "metrics", Listener: metricsListener, Handler: metricsHandler})
} else if options.MetricsListener != nil {
if gatewayOwned {
_ = gatewayListener.Close()
}
return nil, ErrInvalidOptions
}
return &httpRuntime{options: options.HTTP, handler: handler, endpoints: endpoints}, nil
}
func (runtime *httpRuntime) Run(ctx context.Context) error {
if runtime == nil || runtime.handler == nil || len(runtime.endpoints) == 0 || ctx == nil {
return ErrInvalidOptions
}
completed := make(chan struct{})
go func() {
select {
case <-ctx.Done():
shutdownContext, cancel := context.WithTimeout(context.Background(), shutdownTimeout(runtime.options))
defer cancel()
_ = runtime.handler.Shutdown(shutdownContext)
case <-completed:
}
}()
err := httpserver.Serve(ctx, runtime.options, runtime.endpoints...)
close(completed)
return err
}
func resolveListener(ctx context.Context, address string, listener net.Listener) (net.Listener, bool, error) {
if listener != nil {
return listener, false, nil
}
resolved, err := (&net.ListenConfig{}).Listen(ctx, "tcp", address)
if err != nil {
return nil, false, err
}
return resolved, true, nil
}
func shutdownTimeout(options httpserver.Options) time.Duration {
if options.ShutdownTimeout > 0 {
return options.ShutdownTimeout
}
return httpserver.DefaultOptions().ShutdownTimeout
}
type snapshotReadiness struct {
store *snapshot.Store
now func() time.Time
}
func (readiness snapshotReadiness) Ready(ctx context.Context) error {
if ctx == nil || readiness.store == nil || readiness.now == nil {
return ErrNotReady
}
if err := ctx.Err(); err != nil {
return err
}
current := readiness.store.Current()
if current == nil || current.ValidUntil.IsZero() || !current.ValidUntil.After(readiness.now().UTC()) {
return ErrNotReady
}
return nil
}
func controlPlaneTransport(
configuration config.ControlPlane,
address string,
override credentials.TransportCredentials,
) (credentials.TransportCredentials, error) {
if override != nil {
return override, nil
}
switch configuration.TLS.Mode {
case "disabled":
if !loopbackAddress(address) {
return nil, fmt.Errorf("%w: plaintext control plane target must be loopback", ErrInvalidOptions)
}
return insecure.NewCredentials(), nil
case "mtls":
if configuration.GatewayTLS.CertFile == "" || configuration.GatewayTLS.KeyFile == "" || configuration.GatewayTLS.ServerCAFile == "" {
return nil, fmt.Errorf("%w: controlPlane.gatewayTLS is required for mtls", ErrInvalidOptions)
}
certificate, err := tls.LoadX509KeyPair(configuration.GatewayTLS.CertFile, configuration.GatewayTLS.KeyFile)
if err != nil {
return nil, fmt.Errorf("load gateway control plane certificate: %w", err)
}
caPEM, err := os.ReadFile(configuration.GatewayTLS.ServerCAFile)
if err != nil {
return nil, fmt.Errorf("read gateway control plane CA: %w", err)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(caPEM) {
return nil, errors.New("parse gateway control plane CA")
}
host, _, _ := net.SplitHostPort(address)
return credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, RootCAs: roots,
ServerName: strings.Trim(host, "[]"),
}), nil
default:
return nil, fmt.Errorf("%w: unsupported control plane tls mode", ErrInvalidOptions)
}
}
func validDialAddress(address string) bool {
host, port, err := net.SplitHostPort(address)
if err != nil || host == "" {
return false
}
value, err := strconv.ParseUint(port, 10, 16)
if err != nil || value == 0 {
return false
}
parsed := net.ParseIP(strings.Trim(host, "[]"))
return parsed == nil || !parsed.IsUnspecified()
}
func loopbackAddress(address string) bool {
host, _, err := net.SplitHostPort(address)
if err != nil {
return false
}
host = strings.Trim(host, "[]")
if strings.EqualFold(host, "localhost") {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func nilInterface(value any) bool {
if value == nil {
return true
}
reflected := reflect.ValueOf(value)
switch reflected.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return reflected.IsNil()
default:
return false
}
}

View File

@ -0,0 +1,296 @@
package bootstrap
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"time"
controlplanev1 "proxy-pool/gen/controlplane/v1"
"proxy-pool/internal/config"
"proxy-pool/internal/controlplane/snapshotwire"
"proxy-pool/internal/gateway/snapshot"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestRunServesGatewayOnlyAfterApplyingControlPlaneSnapshot(t *testing.T) {
t.Parallel()
target := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/through-gateway" {
t.Fatalf("target path = %q, want /through-gateway", request.URL.Path)
}
_, _ = io.WriteString(writer, "direct-route")
}))
t.Cleanup(target.Close)
controlListener := mustListen(t)
controlServer := grpc.NewServer()
controlplanev1.RegisterWorkerControlPlaneServer(controlServer, &snapshotServer{
snapshot: testSnapshot(t, target.URL),
})
go func() { _ = controlServer.Serve(controlListener) }()
t.Cleanup(func() {
controlServer.Stop()
_ = controlListener.Close()
})
proxyListener := mustListen(t)
metricsListener := mustListen(t)
configPath := writeConfig(t, target.URL)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
result := make(chan error, 1)
go func() {
result <- Run(ctx, Options{
ConfigPath: configPath,
Resolver: config.OSResolver{},
ControlPlaneAddress: controlListener.Addr().String(),
ClusterID: "cluster-a",
WorkerID: "worker-a",
InstanceID: "instance-a",
Zone: "zone-a",
GatewayListener: proxyListener,
MetricsListener: metricsListener,
GRPCTransport: insecure.NewCredentials(),
ReconnectInitialDelay: 5 * time.Millisecond,
ReconnectMaxDelay: 20 * time.Millisecond,
})
}()
if err := waitForStatus("http://"+metricsListener.Addr().String()+"/readyz", http.StatusOK); err != nil {
select {
case runErr := <-result:
t.Fatalf("gateway did not become ready: %v; Run() error = %v", err, runErr)
default:
t.Fatalf("gateway did not become ready: %v", err)
}
}
proxyURL, err := url.Parse("http://" + proxyListener.Addr().String())
if err != nil {
t.Fatalf("Parse(proxy URL): %v", err)
}
response, err := (&http.Client{Timeout: time.Second, Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}}).Get(target.URL + "/through-gateway")
if err != nil {
t.Fatalf("GET through gateway: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
t.Fatalf("ReadAll(gateway response): %v", err)
}
if response.StatusCode != http.StatusOK || string(body) != "direct-route" {
t.Fatalf("gateway response = (%d, %q)", response.StatusCode, body)
}
cancel()
select {
case err := <-result:
if err != nil && !errors.Is(err, context.Canceled) {
t.Fatalf("Run() error = %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Run() did not stop after cancellation")
}
}
func TestRunRejectsMissingControlPlaneEndpoint(t *testing.T) {
t.Parallel()
err := Run(context.Background(), Options{
ConfigPath: "config.yaml", Resolver: config.OSResolver{},
ClusterID: "cluster-a", WorkerID: "worker-a", InstanceID: "instance-a", Zone: "zone-a",
})
if !errors.Is(err, ErrInvalidOptions) {
t.Fatalf("Run() error = %v, want ErrInvalidOptions", err)
}
}
func TestControlPlaneTransportRequiresDedicatedGatewayTLS(t *testing.T) {
t.Parallel()
_, err := controlPlaneTransport(config.ControlPlane{
TLS: config.ControlPlaneTLS{Mode: "mtls"},
}, "controller.example:8443", nil)
if !errors.Is(err, ErrInvalidOptions) {
t.Fatalf("controlPlaneTransport() error = %v, want ErrInvalidOptions", err)
}
}
func TestSnapshotReadinessRequiresCurrentSnapshot(t *testing.T) {
t.Parallel()
store := snapshot.NewStore("cluster-a", "worker-a")
now := time.Now().UTC()
readiness := snapshotReadiness{store: store, now: func() time.Time { return now }}
if err := readiness.Ready(context.Background()); !errors.Is(err, ErrNotReady) {
t.Fatalf("Ready() before snapshot error = %v, want ErrNotReady", err)
}
if err := store.Apply(snapshot.Envelope{
ClusterID: "cluster-a", WorkerID: "worker-a", Epoch: 1, Version: 1, Full: true,
Checksum: snapshot.ChecksumWithRouting(nil, nil), ValidUntil: now.Add(time.Second),
}); err != nil {
t.Fatalf("Apply() error = %v", err)
}
if err := readiness.Ready(context.Background()); err != nil {
t.Fatalf("Ready() after snapshot error = %v", err)
}
now = now.Add(2 * time.Second)
if err := readiness.Ready(context.Background()); !errors.Is(err, ErrNotReady) {
t.Fatalf("Ready() after expiry error = %v, want ErrNotReady", err)
}
}
type snapshotServer struct {
controlplanev1.UnimplementedWorkerControlPlaneServer
snapshot *controlplanev1.WorkerSnapshot
}
func (server *snapshotServer) RegisterWorker(_ context.Context, request *controlplanev1.RegisterWorkerRequest) (*controlplanev1.RegisterWorkerResponse, error) {
if request.GetWorkerId() != "worker-a" || request.GetInstanceId() != "instance-a" || request.GetZone() != "zone-a" {
return nil, errors.New("unexpected worker registration")
}
return &controlplanev1.RegisterWorkerResponse{
WorkerId: request.GetWorkerId(), SessionId: "session-a", OwnershipEpoch: 1,
HeartbeatInterval: durationpb.New(time.Hour), MaxStaleAge: durationpb.New(2 * time.Hour),
}, nil
}
func (server *snapshotServer) WatchSnapshots(_ *controlplanev1.WatchSnapshotsRequest, stream grpc.ServerStreamingServer[controlplanev1.SnapshotEnvelope]) error {
if err := stream.Send(&controlplanev1.SnapshotEnvelope{Payload: &controlplanev1.SnapshotEnvelope_Full{Full: server.snapshot}}); err != nil {
return err
}
<-stream.Context().Done()
return stream.Context().Err()
}
func (server *snapshotServer) AcknowledgeSnapshot(context.Context, *controlplanev1.AcknowledgeSnapshotRequest) (*emptypb.Empty, error) {
return &emptypb.Empty{}, nil
}
func (server *snapshotServer) ReportRuntime(context.Context, *controlplanev1.ReportRuntimeRequest) (*controlplanev1.ReportRuntimeResponse, error) {
return &controlplanev1.ReportRuntimeResponse{AcceptedOwnershipEpoch: 1}, nil
}
func testSnapshot(t *testing.T, _ string) *controlplanev1.WorkerSnapshot {
t.Helper()
generated := time.Now().UTC()
result := &controlplanev1.WorkerSnapshot{
Version: 1, OwnershipEpoch: 1, GeneratedAt: timestamppb.New(generated), ValidUntil: timestamppb.New(generated.Add(time.Minute)),
Routing: []*controlplanev1.RoutingRule{{
Name: "gateway-default", Enabled: true, HostRegex: ".+", Upstreams: []string{"provider-a"},
Strategy: &controlplanev1.RoutingStrategy{Type: controlplanev1.StrategyType_STRATEGY_TYPE_RANDOM},
OnUnavailable: controlplanev1.UnavailableAction_UNAVAILABLE_ACTION_DIRECT,
}},
}
checksum, err := snapshotwire.Checksum(result)
if err != nil {
t.Fatalf("Checksum() error = %v", err)
}
result.Checksum = checksum[:]
return result
}
func writeConfig(t *testing.T, target string) string {
t.Helper()
parsed, err := url.Parse(target)
if err != nil {
t.Fatalf("Parse(target URL): %v", err)
}
port := parsed.Port()
if port == "" {
t.Fatalf("target URL has no explicit port: %q", target)
}
path := filepath.Join(t.TempDir(), "gateway.yaml")
content := fmt.Sprintf(`version: 1
security:
requireProtectionOnPublicListen: false
gateway:
enabled: true
listen: 127.0.0.1:0
auth: {mode: none}
destinationPolicy:
denyPrivateNetworks: false
denyLoopback: false
denyLinkLocal: false
allowedPorts: [%s]
distribution:
enabled: false
admin:
enabled: false
controlPlane:
enabled: true
listen: 127.0.0.1:0
protocolVersion: 1
heartbeatInterval: 1s
sessionTTL: 3s
maxStaleAge: 2s
maxMessageBytes: 1048576
maxRuntimeCounters: 10
maxConcurrentStreams: 10
tls: {mode: disabled}
metrics:
enabled: true
listen: 127.0.0.1:0
upstreams:
provider-a:
enabled: true
exposure: [gateway]
provider: {billingMode: fetch, protocols: [http]}
api: {url: https://provider.invalid/api, method: GET, auth: {type: none}}
proxyAuth: {type: response}
pool: {maxSize: 1}
capacity: {maxConcurrencyPerProxy: 2}
refill: {reconcileInterval: 1s, minimumAvailableSlots: 1, targetAvailableSlots: 2}
lifecycle: {ttl: 1m, allocationSafetyMargin: 1s}
fetch: {estimatedIPsPerCall: 1, timeout: 1s, maxAttempts: 1, maxInFlight: 1}
`, port)
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("WriteFile(): %v", err)
}
return path
}
func mustListen(t *testing.T) net.Listener {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Listen(): %v", err)
}
return listener
}
func waitForStatus(target string, want int) error {
deadline := time.Now().Add(2 * time.Second)
last := "no response"
for time.Now().Before(deadline) {
response, err := (&http.Client{Timeout: 100 * time.Millisecond}).Get(target)
if err == nil {
_ = response.Body.Close()
if response.StatusCode == want {
return nil
}
last = response.Status
} else {
last = err.Error()
}
time.Sleep(10 * time.Millisecond)
}
return fmt.Errorf("GET %s did not return %d: last result %s", target, want, last)
}

View File

@ -126,6 +126,10 @@ func (watcher *SnapshotWatcher) applyFull(full *controlplanev1.WorkerSnapshot) e
if err != nil { if err != nil {
return err return err
} }
credentials, err := wireSnapshotCredentials(full.GetCredentials(), proxies)
if err != nil {
return err
}
routes, err := wireRouting(full.GetRouting()) routes, err := wireRouting(full.GetRouting())
if err != nil { if err != nil {
return err return err
@ -133,12 +137,42 @@ func (watcher *SnapshotWatcher) applyFull(full *controlplanev1.WorkerSnapshot) e
envelope := snapshot.Envelope{ envelope := snapshot.Envelope{
ClusterID: watcher.options.ClusterID, WorkerID: watcher.options.WorkerID, ClusterID: watcher.options.ClusterID, WorkerID: watcher.options.WorkerID,
Epoch: full.GetOwnershipEpoch(), Version: full.GetVersion(), Full: true, ValidUntil: validUntil, Epoch: full.GetOwnershipEpoch(), Version: full.GetVersion(), Full: true, ValidUntil: validUntil,
Proxies: proxies, Routing: routes, Proxies: proxies, Routing: routes, Credentials: credentials,
} }
envelope.Checksum = snapshot.ChecksumWithRouting(proxies, routes) envelope.Checksum = snapshot.ChecksumWithCredentials(proxies, routes, credentials)
return watcher.store.Apply(envelope) return watcher.store.Apply(envelope)
} }
func wireSnapshotCredentials(source []*controlplanev1.SnapshotCredential, proxies []proxyDomain.Proxy) ([]snapshot.Credential, error) {
result := make([]snapshot.Credential, len(source))
indexed := make(map[string]struct{}, len(source))
for index, item := range source {
if item == nil || !workerruntime.ValidIdentifier(item.GetSecretRef()) || !workerruntime.ValidIdentifier(item.GetCredentialVersion()) ||
(item.GetUsername() == "" && item.GetPassword() == "") {
return nil, ErrInvalidSnapshotWatcher
}
key := item.GetSecretRef() + "\x00" + item.GetCredentialVersion()
if _, duplicate := indexed[key]; duplicate {
return nil, ErrInvalidSnapshotWatcher
}
indexed[key] = struct{}{}
result[index] = snapshot.Credential{
SecretRef: item.GetSecretRef(), CredentialVersion: item.GetCredentialVersion(),
Username: item.GetUsername(), Password: item.GetPassword(),
}
}
for _, proxy := range proxies {
if proxy.SecretRef == "" && proxy.CredentialVersion == "" {
continue
}
key := proxy.SecretRef + "\x00" + proxy.CredentialVersion
if _, exists := indexed[key]; !exists {
return nil, ErrInvalidSnapshotWatcher
}
}
return result, nil
}
func wireRouting(source []*controlplanev1.RoutingRule) ([]routingDomain.Rule, error) { func wireRouting(source []*controlplanev1.RoutingRule) ([]routingDomain.Rule, error) {
result := make([]routingDomain.Rule, 0, len(source)) result := make([]routingDomain.Rule, 0, len(source))
names := make(map[string]struct{}, len(source)) names := make(map[string]struct{}, len(source))

View File

@ -23,7 +23,11 @@ func TestSnapshotWatcherAppliesVerifiedFullSnapshotAndAcknowledges(t *testing.T)
Version: 1, OwnershipEpoch: 7, GeneratedAt: timestamppb.New(time.Now()), ValidUntil: timestamppb.New(time.Now().Add(time.Minute)), Version: 1, OwnershipEpoch: 7, GeneratedAt: timestamppb.New(time.Now()), ValidUntil: timestamppb.New(time.Now().Add(time.Minute)),
Proxies: []*controlplanev1.OwnedProxy{{ Proxies: []*controlplanev1.OwnedProxy{{
Id: "proxy-a", Upstream: "upstream-a", Protocol: controlplanev1.ProxyProtocol_PROXY_PROTOCOL_HTTP, Id: "proxy-a", Upstream: "upstream-a", Protocol: controlplanev1.ProxyProtocol_PROXY_PROTOCOL_HTTP,
Host: "192.0.2.10", Port: 8080, MaxConcurrency: 3, ExpiresAt: timestamppb.New(time.Now().Add(time.Minute)), Host: "192.0.2.10", Port: 8080, Username: "upstream", SecretRef: "cred_a", CredentialVersion: "v1",
MaxConcurrency: 3, ExpiresAt: timestamppb.New(time.Now().Add(time.Minute)),
}},
Credentials: []*controlplanev1.SnapshotCredential{{
SecretRef: "cred_a", CredentialVersion: "v1", Username: "upstream", Password: "secret",
}}, }},
Routing: []*controlplanev1.RoutingRule{{ Routing: []*controlplanev1.RoutingRule{{
Name: "gateway-api", Enabled: true, HostRegex: `^api\.example\.test$`, Upstreams: []string{"upstream-a"}, Name: "gateway-api", Enabled: true, HostRegex: `^api\.example\.test$`, Upstreams: []string{"upstream-a"},
@ -54,6 +58,10 @@ func TestSnapshotWatcherAppliesVerifiedFullSnapshotAndAcknowledges(t *testing.T)
if client.watch.GetSessionId() != "session-a" || client.ack.GetVersion() != 1 || !client.ack.GetApplied() || string(client.ack.GetChecksum()) != string(full.GetChecksum()) { if client.watch.GetSessionId() != "session-a" || client.ack.GetVersion() != 1 || !client.ack.GetApplied() || string(client.ack.GetChecksum()) != string(full.GetChecksum()) {
t.Fatalf("watch=%+v ack=%+v", client.watch, client.ack) t.Fatalf("watch=%+v ack=%+v", client.watch, client.ack)
} }
credential, err := store.Credential(context.Background(), view.Entries[0].Proxy)
if err != nil || credential.Username != "upstream" || credential.Password != "secret" {
t.Fatalf("Credential() = (%+v, %v)", credential, err)
}
} }
func TestSnapshotWatcherRejectsChecksumAndAcknowledgesFailure(t *testing.T) { func TestSnapshotWatcherRejectsChecksumAndAcknowledgesFailure(t *testing.T) {

View File

@ -277,7 +277,6 @@ func proxyDescriptor(t *testing.T, id, rawURL string) proxyDomain.Proxy {
SourceUpstream: "provider-a", SourceUpstream: "provider-a",
MaxConcurrency: 2, MaxConcurrency: 2,
State: proxyDomain.StateAvailable, State: proxyDomain.StateAvailable,
CredentialVersion: "v1",
} }
} }

View File

@ -595,7 +595,6 @@ func dispatcherWithProxies(t *testing.T, ids ...string) (*dispatch.Dispatcher, *
SourceUpstream: "provider-a", SourceUpstream: "provider-a",
MaxConcurrency: 2, MaxConcurrency: 2,
State: proxyDomain.StateAvailable, State: proxyDomain.StateAvailable,
CredentialVersion: "v1",
}) })
} }
envelope := snapshot.Envelope{ envelope := snapshot.Envelope{

View File

@ -1,6 +1,7 @@
package snapshot package snapshot
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@ -24,6 +25,8 @@ var (
ErrInvalidRuntimeReport = errors.New("invalid worker runtime report") ErrInvalidRuntimeReport = errors.New("invalid worker runtime report")
ErrInvalidRuntimeLimit = errors.New("invalid snapshot runtime limit") ErrInvalidRuntimeLimit = errors.New("invalid snapshot runtime limit")
ErrRuntimeLimitExceeded = errors.New("snapshot runtime limit exceeded") ErrRuntimeLimitExceeded = errors.New("snapshot runtime limit exceeded")
ErrInvalidCredential = errors.New("invalid snapshot credential")
ErrCredentialMissing = errors.New("snapshot credential is missing")
) )
const defaultRuntimeLimit = 1_000_000 const defaultRuntimeLimit = 1_000_000
@ -93,15 +96,29 @@ type runtimeRegistration struct {
} }
type Envelope struct { type Envelope struct {
ClusterID string ClusterID string
WorkerID string WorkerID string
Epoch uint64 Epoch uint64
Version uint64 Version uint64
Full bool Full bool
Checksum string Checksum string
ValidUntil time.Time ValidUntil time.Time
Proxies []proxyDomain.Proxy Proxies []proxyDomain.Proxy
Routing []routing.Rule Routing []routing.Rule
Credentials []Credential
}
// Credential is ephemeral material received through a verified mTLS snapshot.
// Its Format method prevents accidental diagnostics from exposing either value.
type Credential struct {
SecretRef string
CredentialVersion string
Username string
Password string
}
func (Credential) Format(state fmt.State, _ rune) {
_, _ = state.Write([]byte("snapshot.Credential{SecretRef:<redacted>, CredentialVersion:<redacted>, Username:<redacted>, Password:<redacted>}"))
} }
type Entry struct { type Entry struct {
@ -110,14 +127,15 @@ type Entry struct {
} }
type View struct { type View struct {
ClusterID string ClusterID string
WorkerID string WorkerID string
Epoch uint64 Epoch uint64
Version uint64 Version uint64
Checksum string Checksum string
ValidUntil time.Time ValidUntil time.Time
Entries []Entry Entries []Entry
routing *routing.RuleSet routing *routing.RuleSet
credentials map[string]Credential
all []int all []int
byScheme map[proxyDomain.Scheme][]int byScheme map[proxyDomain.Scheme][]int
@ -229,13 +247,17 @@ func (s *Store) Apply(envelope Envelope) error {
if !envelope.Full || envelope.Epoch == 0 || envelope.Version == 0 { if !envelope.Full || envelope.Epoch == 0 || envelope.Version == 0 {
return ErrResyncRequired return ErrResyncRequired
} }
if envelope.Checksum != ChecksumWithRouting(envelope.Proxies, envelope.Routing) { if envelope.Checksum != ChecksumWithCredentials(envelope.Proxies, envelope.Routing, envelope.Credentials) {
return ErrChecksumMismatch return ErrChecksumMismatch
} }
routes, err := routing.Compile(envelope.Routing) routes, err := routing.Compile(envelope.Routing)
if err != nil { if err != nil {
return fmt.Errorf("compile snapshot routing: %w", err) return fmt.Errorf("compile snapshot routing: %w", err)
} }
credentials, err := indexCredentials(envelope.Credentials, envelope.Proxies)
if err != nil {
return err
}
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -315,20 +337,56 @@ func (s *Store) Apply(envelope Envelope) error {
} }
next := &View{ next := &View{
ClusterID: envelope.ClusterID, ClusterID: envelope.ClusterID,
WorkerID: envelope.WorkerID, WorkerID: envelope.WorkerID,
Epoch: envelope.Epoch, Epoch: envelope.Epoch,
Version: envelope.Version, Version: envelope.Version,
Checksum: envelope.Checksum, Checksum: envelope.Checksum,
ValidUntil: envelope.ValidUntil.UTC(), ValidUntil: envelope.ValidUntil.UTC(),
Entries: entries, Entries: entries,
routing: routes, routing: routes,
credentials: credentials,
} }
next.buildIndexes() next.buildIndexes()
s.current.Store(next) s.current.Store(next)
return nil return nil
} }
// Credential resolves one exact Proxy credential reference from the current
// immutable view. Credentialless proxies preserve their metadata username.
func (s *Store) Credential(ctx context.Context, selected proxyDomain.Proxy) (Credential, error) {
if ctx == nil {
return Credential{}, context.Canceled
}
if err := ctx.Err(); err != nil {
return Credential{}, err
}
if (selected.SecretRef == "") != (selected.CredentialVersion == "") {
return Credential{}, ErrInvalidCredential
}
if selected.SecretRef == "" {
return Credential{Username: selected.Username}, nil
}
if s == nil {
return Credential{}, ErrCredentialMissing
}
current := s.current.Load()
if current == nil {
return Credential{}, ErrCredentialMissing
}
credential, ok := current.credentials[credentialKey(selected.SecretRef, selected.CredentialVersion)]
if !ok {
return Credential{}, ErrCredentialMissing
}
if selected.Username != "" && credential.Username != "" && selected.Username != credential.Username {
return Credential{}, ErrInvalidCredential
}
if credential.Username == "" {
credential.Username = selected.Username
}
return credential, nil
}
// MatchRouting matches a request against the immutable rules published with // MatchRouting matches a request against the immutable rules published with
// this proxy view. A View is the atomic consistency boundary for both sets. // this proxy view. A View is the atomic consistency boundary for both sets.
func (v *View) MatchRouting(request routing.Request) (routing.Rule, bool) { func (v *View) MatchRouting(request routing.Request) (routing.Rule, bool) {
@ -427,12 +485,18 @@ func Checksum(proxies []proxyDomain.Proxy) string {
} }
func ChecksumWithRouting(proxies []proxyDomain.Proxy, rules []routing.Rule) string { func ChecksumWithRouting(proxies []proxyDomain.Proxy, rules []routing.Rule) string {
return ChecksumWithCredentials(proxies, rules, nil)
}
func ChecksumWithCredentials(proxies []proxyDomain.Proxy, rules []routing.Rule, credentials []Credential) string {
payload := struct { payload := struct {
Proxies []proxyDomain.Proxy `json:"proxies"` Proxies []proxyDomain.Proxy `json:"proxies"`
Routing []routing.Rule `json:"routing"` Routing []routing.Rule `json:"routing"`
Credentials []Credential `json:"credentials"`
}{ }{
Proxies: cloneAndSort(proxies), Proxies: cloneAndSort(proxies),
Routing: cloneRoutingRules(rules), Routing: cloneRoutingRules(rules),
Credentials: cloneAndSortCredentials(credentials),
} }
encoded, err := json.Marshal(payload) encoded, err := json.Marshal(payload)
if err != nil { if err != nil {
@ -442,6 +506,46 @@ func ChecksumWithRouting(proxies []proxyDomain.Proxy, rules []routing.Rule) stri
return hex.EncodeToString(digest[:]) return hex.EncodeToString(digest[:])
} }
func indexCredentials(source []Credential, proxies []proxyDomain.Proxy) (map[string]Credential, error) {
indexed := make(map[string]Credential, len(source))
for _, credential := range source {
if credential.SecretRef == "" || credential.CredentialVersion == "" || (credential.Username == "" && credential.Password == "") {
return nil, ErrInvalidCredential
}
key := credentialKey(credential.SecretRef, credential.CredentialVersion)
if _, duplicate := indexed[key]; duplicate {
return nil, ErrInvalidCredential
}
indexed[key] = credential
}
for _, proxy := range proxies {
if (proxy.SecretRef == "") != (proxy.CredentialVersion == "") {
return nil, ErrInvalidCredential
}
if proxy.SecretRef == "" {
continue
}
credential, exists := indexed[credentialKey(proxy.SecretRef, proxy.CredentialVersion)]
if !exists || (proxy.Username != "" && credential.Username != "" && proxy.Username != credential.Username) {
return nil, ErrInvalidCredential
}
}
return indexed, nil
}
func credentialKey(secretRef, version string) string { return secretRef + "\x00" + version }
func cloneAndSortCredentials(source []Credential) []Credential {
cloned := append([]Credential(nil), source...)
sort.Slice(cloned, func(left, right int) bool {
if cloned[left].SecretRef == cloned[right].SecretRef {
return cloned[left].CredentialVersion < cloned[right].CredentialVersion
}
return cloned[left].SecretRef < cloned[right].SecretRef
})
return cloned
}
func cloneRoutingRules(source []routing.Rule) []routing.Rule { func cloneRoutingRules(source []routing.Rule) []routing.Rule {
result := make([]routing.Rule, len(source)) result := make([]routing.Rule, len(source))
for index, rule := range source { for index, rule := range source {

View File

@ -1,9 +1,11 @@
package snapshot package snapshot
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"testing" "testing"
"time" "time"
@ -126,6 +128,35 @@ func TestStorePublishesRoutingWithTheSameSnapshotVersion(t *testing.T) {
} }
} }
func TestStoreResolvesCredentialOnlyFromCurrentSnapshot(t *testing.T) {
store := NewStore("cluster-a", "worker-a")
proxy := proxyDomain.Proxy{
ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080, MaxConcurrency: 1,
SecretRef: "cred_a", CredentialVersion: "v1", Username: "upstream",
}
envelope := Envelope{
ClusterID: "cluster-a", WorkerID: "worker-a", Epoch: 1, Version: 1, Full: true, Proxies: []proxyDomain.Proxy{proxy},
Credentials: []Credential{{SecretRef: "cred_a", CredentialVersion: "v1", Username: "upstream", Password: "secret"}},
}
envelope.Checksum = ChecksumWithCredentials(envelope.Proxies, envelope.Routing, envelope.Credentials)
if err := store.Apply(envelope); err != nil {
t.Fatalf("Apply(): %v", err)
}
credential, err := store.Credential(context.Background(), proxy)
if err != nil || credential.Username != "upstream" || credential.Password != "secret" {
t.Fatalf("Credential() = (%+v, %v)", credential, err)
}
if formatted := fmt.Sprintf("%+v", credential); strings.Contains(formatted, "secret") || strings.Contains(formatted, "upstream") {
t.Fatalf("formatted credential exposes material: %s", formatted)
}
missing := Envelope{ClusterID: "cluster-a", WorkerID: "worker-a", Epoch: 1, Version: 2, Full: true, Proxies: []proxyDomain.Proxy{proxy}}
missing.Checksum = ChecksumWithCredentials(missing.Proxies, nil, nil)
if err := store.Apply(missing); !errors.Is(err, ErrInvalidCredential) {
t.Fatalf("Apply(missing credential) error = %v, want ErrInvalidCredential", err)
}
}
func TestViewSelectFiltersBySchemeUpstreamTagAndExclude(t *testing.T) { func TestViewSelectFiltersBySchemeUpstreamTagAndExclude(t *testing.T) {
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC) now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
expiresSoon := now.Add(5 * time.Second) expiresSoon := now.Add(5 * time.Second)