Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `docs/protocols.md` / `docs/protocols_zh.md`: developer-facing reference for the tunnel protocols and their remote-attestation handshake, covering the ohttp tunnel's outer request routing, key-config distribution, request encapsulation, and the background-check and passport attestation flows for both directions, with sequence diagrams in a formal RFC 9334-style notation.
- RATS-TLS chapter in `docs/protocols.md` / `docs/protocols_zh.md`: the standard-TLS-1.3 handshake with RA material carried in the self-signed DICE certificate, the four direction combinations, multiplexed versus non-multiplexed data planes, the freshness and session-resumption model, and handshake failure behavior, in the same notation as the ohttp chapter.
- `trusted-network-gateway-<version>-1.el7.x86_64.rpm` release asset built on a glibc 2.17 (manylinux2014) baseline: it installs on any RPM-based distro with glibc 2.17 or newer, including AnolisOS 7.9, alongside the existing an8 package.
- OHTTP `aead_suites` policy on ingress and egress: a per-endpoint, ordered allowlist of HPKE AEAD suites (`aes128_gcm`, `aes256_gcm`, `chacha20_poly1305`). On the egress it sets both the advertised suites and the decapsulation allowlist, and a disallowed suite is rejected with HTTP 422 so a TNG ingress refetches its key config. On the ingress it also sets the client preference order, which takes precedence over the server's advertised order. Unset keeps the previous behavior; the egress default now advertises AES-256-GCM as well, and an unset ingress still selects the first advertised suite.

### Changed

Expand Down
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,26 @@ on:
- "master"
```

## Naming

A name is a promise about scope. It must match what the field actually selects, no wider and no narrower.

- Derive the noun from the value space. If only AEADs are selectable, `aead_suites` is right; `cipher_suites` (RFC 9180 defines it as KEM + KDF + AEAD) and `hpke_suites` (suggests the KEM is selectable) over-promise.
- Do not borrow a standard term whose meaning spans more than the knob does. In RFC 9458, `symmetric_algorithms` covers KDF and AEAD, so an AEAD-only list must not use `symmetric`.
- Avoid collisions with established terms from adjacent layers (TLS `cipher_suites`) even when the familiar name reads better.
- Design for the growth axis. If KDF or KEM may become configurable later, add a sibling field (`kdf`) instead of widening the existing name. Additive beats redefinition.
- Values must deliver what the name promises. If the name needs a sentence explaining what it does not cover, it is the wrong name.

## Validation

Validate a value where it is created, in the layer that owns the invariant. A pass over the whole config is a last resort, not the default.

- Prefer a type that cannot hold an invalid value (a newtype with a fallible constructor or a custom deserializer) over a later `validate()` pass. Every consumer then receives an already-valid value.
- Keep the check in the module that owns the type. A feature-specific field does not belong in a global aggregate's validation hook; a shared entry point should not enumerate each module's rules.
- Do not hand-build location strings for error messages. Parsing and type errors already carry the location; a hand-written path rots when the shape changes.
- Put one feature's types in a module named for what they own, not in the generic entry module.
- Why: a global pass couples every feature to the entry point and is easy to bypass when a new construction path appears. An invariant held by the type cannot be bypassed.

## API Compatibility

When designing public APIs (REST endpoints, config fields, trait methods, public structs/traits), always design the full namespace structure upfront — do not defer naming decisions. Retrofitting a namespace layer later (e.g. inserting `/ohttp/` into an existing path like `/status/egress/<id>/keys` → `/status/egress/<id>/ohttp/keys`) breaks backward compatibility and is extremely costly. If a category of resources might grow multiple sub-resources in the future, include that category's namespace from day one.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ Enable OHTTP in `add_ingress` by specifying the `ohttp` field.
|---|---|---|---|
| `path_rewrites` | array [[PathRewrite](#pathrewrite)] | `[]` | Path rewrite rule list, matched in order |
| `path_default` | string | `"root"` | Fallback outer path when no `path_rewrites` rule matches (unset/empty/no match). `"root"` → `/`; `"original"` → the inner request's original path |
| `aead_suites` | array [string] | — | HPKE AEAD suites this ingress may use, in preference order. See `aead_suites` below. |

#### PathRewrite

Expand Down Expand Up @@ -1224,6 +1225,19 @@ Whether to wrap the OHTTP-encrypted request in TLS (HTTPS) when forwarding to th

PEM file paths of trusted CA certificates used to verify the upstream's TLS certificate when `tls: true`. Each file may contain a single certificate or a PEM bundle (multiple concatenated certs). Useful for self-signed or internal-CA gateways. Empty (the default) relies on the built-in webpki roots. Ignored unless `tls: true`.

#### `aead_suites` (optional, list of string)

Restricts the HPKE AEAD suites this ingress may use, in preference order. Legal values are `aes128_gcm`, `aes256_gcm` and `chacha20_poly1305`; matching is case-sensitive and aliases are rejected. Omit the field to keep the historical behavior: no filtering, and the first suite advertised by the server's first key config. This is a compliance control, not a performance tuning option.

When set, the ingress intersects each server key config's advertised suites with this list, picks the first server key config (in server order) with a non-empty intersection, then picks the first suite in this list's order. Taking the client's order rather than the server's first advertised suite is a deliberate deviation from RFC 9458. Only TNG as ingress behaves this way; standard OHTTP servers are unaffected.

If no server key config intersects the list, the key fetch fails and requests error out. The ingress never falls back to the default suites. An empty list or a duplicate entry is rejected at config load. The policy is read once at startup, so changing it requires restarting the ingress.

This field only constrains the TNG ingress that carries it. To constrain every client of an egress, configure `aead_suites` on the egress as well.

In a `peer_shared` cluster, each node applies its own policy. Use the same list on every node: a node whose policy differs rejects the suites its peers advertise, and clients keep failing until they refetch a key config they can use.

When tightening to AES-256-GCM only, first upgrade the egress and every ingress to a version that supports AES-256-GCM, then roll out the restrictive policy. A client still holding a cached key config with an older suite keeps failing until it refetches; the egress's HTTP 422 response makes the ingress invalidate its cache and refetch on the next request.

<a name="ohttp-egress-side-configuration"></a>

Expand All @@ -1236,6 +1250,7 @@ Corresponding to Ingress, enable OHTTP in `add_egress` by specifying the `ohttp`
| `cors` | [CorsConfig](#corsconfig) | None | CORS configuration for browser access to OHTTP endpoints |
| `key` | [KeyConfig](#key-management) | None | Key management configuration (see [Key Management](#key-management) below) |
| `forward_client_ip` | bool | `true` | When `true`, the egress injects `X-Real-IP` (set to the direct TCP peer) and `X-Forwarded-For` (append the direct peer) into the decrypted inner request before forwarding upstream. The direct peer is whoever delivered the OHTTP POST, which in the relay topology is the relay, not the real client. Set `false` to disable. |
| `aead_suites` | array [string] | `["aes128_gcm", "aes256_gcm", "chacha20_poly1305"]` | HPKE AEAD suites to advertise and accept. See `aead_suites` below. |

> [!NOTE]
> `allow_non_tng_traffic_regexes` is deprecated since 2.2.4; use `direct_forward` instead.
Expand Down Expand Up @@ -1315,6 +1330,18 @@ Example:
}
```

#### `aead_suites` (optional, list of string)

The HPKE AEAD suites this egress advertises in its key config and accepts when decapsulating a request. Legal values and defaults match the ingress side: `aes128_gcm`, `aes256_gcm` and `chacha20_poly1305`, defaulting to all three in that order.

Both halves are driven by the same list. Newly generated and file-loaded key configs advertise only these suites, and every decapsulation checks the suite in the request header, including requests sealed under an older key config or under a key synced from a `peer_shared` peer. A disallowed suite is rejected with HTTP 422 and logged with the suite, key id and peer address; the response body carries no suite details. The 422 makes a TNG ingress discard its cached key config and fetch a new one on the next request.

There is no fallback to the default list. An empty list or a duplicate entry is rejected at config load. The policy is read once at startup, so changing it requires restarting the egress.

In a `peer_shared` cluster, key configs are synced without filtering, so nodes with different policies advertise suites their peers reject. Configure the same `aead_suites` list on every node of the group.

Roll out a restrictive policy after upgrading the egress and the expected clients to a version that supports the suite being kept. A client caching a key config from before the change is rejected until it refetches, which the 422 response drives on its next request.

<a name="ohttp-key-management"></a>

### Key Management
Expand Down
28 changes: 28 additions & 0 deletions docs/configuration_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ OHTTP (Oblivious HTTP) 是一种旨在增强隐私保护的网络协议扩展,
|---|---|---|---|
| `path_rewrites` | array [[PathRewrite](#pathrewrite)] | `[]` | Path 重写规则列表,按顺序匹配 |
| `path_default` | string | `"root"` | 当没有 `path_rewrites` 规则命中(未设置/为空/未匹配)时的外层路径回退值。`"root"` → `/`;`"original"` → 内层请求的原始 path |
| `aead_suites` | array [string] | — | 该 ingress 可使用的 HPKE AEAD 套件,按偏好顺序排列。详见下文 `aead_suites`。 |

#### PathRewrite

Expand Down Expand Up @@ -1212,6 +1213,20 @@ OHTTP 加密后的 HTTP 请求遵循以下规则,以便与 L7 负载均衡器

当 `tls: true` 时,用于校验上游 TLS 证书的可信 CA 证书 PEM 文件路径。每个文件可以是单张证书,也可以是 PEM bundle(多张拼接证书)。适用于自签或内网 CA 网关。默认为空,使用内置 webpki 根。仅在 `tls: true` 时生效。

#### `aead_suites`(可选,字符串数组)

限定该 ingress 可使用的 HPKE AEAD 套件,列表顺序即偏好顺序。合法值为 `aes128_gcm`、`aes256_gcm`、`chacha20_poly1305`,大小写敏感,不接受别名。省略该字段时保持原有行为:不做过滤,直接使用服务端第一份 key config 广告的第一个套件。这是合规开关,不是性能调优选项。

配置后,ingress 把每份服务端 key config 的广告套件与本列表求交集,按服务端 key config 的顺序选择第一份交集非空的 config,再在这份 config 内按本列表的顺序选择第一个可用套件。以客户端顺序优先而不是取服务端广告的第一个套件,是对 RFC 9458 字面规则的刻意偏离。只有 TNG 作为 ingress 时如此,标准 OHTTP 服务端不受影响。

如果所有服务端 key config 的交集都为空,key 拉取失败,请求以错误结束,不会回退到默认套件。空数组或重复项在配置加载阶段即报错。策略只在启动时读取,修改后需要重启 ingress。

该字段只约束配置它的这个 TNG ingress。要约束某个 egress 的所有客户端,必须同时在 egress 上配置 `aead_suites`。

在 `peer_shared` 集群中,每个节点各自应用自己的策略。请在所有节点上使用相同的列表:策略不一致的节点会拒绝其他节点广告的套件,客户端在重新拉取到可用的 key config 之前会持续失败。

收紧为只允许 AES-256-GCM 时,先把 egress 和所有 ingress 升级到支持 AES-256-GCM 的版本,再下发限制策略。仍持有旧 key config 的客户端会持续失败,直到重新拉取;egress 返回的 HTTP 422 会让 ingress 失效缓存,并在下一次请求时重新拉取。


<a name="ohttp-egress-侧配置"></a>

Expand All @@ -1224,6 +1239,7 @@ OHTTP 加密后的 HTTP 请求遵循以下规则,以便与 L7 负载均衡器
| `cors` | [CorsConfig](#corsconfig) | 无 | CORS 配置,用于浏览器端访问 OHTTP 端点 |
| `key` | [KeyConfig](#密钥管理) | 无 | 密钥管理配置(见下方 [密钥管理](#密钥管理)) |
| `forward_client_ip` | bool | `true` | 为 `true` 时,egress 在转发解密后的内层请求给上游前,注入 `X-Real-IP`(设为直连 TCP 对端)与 `X-Forwarded-For`(追加直连对端)。直连对端是投递 OHTTP POST 的一方,在 relay 拓扑里即 relay,而非真实客户端。设为 `false` 关闭。 |
| `aead_suites` | array [string] | `["aes128_gcm", "aes256_gcm", "chacha20_poly1305"]` | 广告并接受的 HPKE AEAD 套件。详见下文 `aead_suites`。 |

> [!NOTE]
> `allow_non_tng_traffic_regexes` 在 2.2.4+ 已弃用,请使用 `direct_forward` 替代。
Expand Down Expand Up @@ -1297,6 +1313,18 @@ OHTTP 加密后的 HTTP 请求遵循以下规则,以便与 L7 负载均衡器
}
```

#### `aead_suites`(可选,字符串数组)

该 egress 在 key config 中广告、并在解封请求时接受的 HPKE AEAD 套件。合法值与默认值和 ingress 侧一致:`aes128_gcm`、`aes256_gcm`、`chacha20_poly1305`,默认按此顺序全部启用。

广告与接受由同一份列表决定。新生成和从文件加载的 key config 只广告这些套件,每次解封都会检查请求头中的套件,包括用旧 key config 或从 `peer_shared` 对端同步来的 key 封装的请求。不在列表内的套件返回 HTTP 422,并在日志中记录套件名、key id 和对端地址;响应体不包含套件细节。422 会让 TNG ingress 失效缓存的 key config,并在下一次请求时重新拉取。

不会回退到默认列表。空数组或重复项在配置加载阶段即报错。策略只在启动时读取,修改后需要重启 egress。

在 `peer_shared` 集群中,key config 同步阶段不做过滤,因此策略不同的节点会广告其邻居会拒绝的套件。请在同一群组的所有节点上配置相同的 `aead_suites` 列表。

下发限制策略前,先升级 egress 和预期客户端到支持目标套件的版本。仍缓存改动前 key config 的客户端会被拒绝,直到重新拉取;422 响应会在其下一次请求时触发重新拉取。


<a name="ohttp-密钥管理"></a>

Expand Down
4 changes: 4 additions & 0 deletions tng-testsuite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ path = "tests/http/mapping_multi_rule.rs"
name = "ohttp"
path = "tests/ohttp/ohttp.rs"

[[test]]
name = "suite_policy"
path = "tests/ohttp/suite_policy.rs"

[[test]]
name = "ohttp_egress_hook"
path = "tests/ohttp/ohttp_egress_hook.rs"
Expand Down
10 changes: 8 additions & 2 deletions tng-testsuite/src/task/tng/external/podman.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ impl TngInstance {
])
.args([
"-v", "/tmp/:/tmp/"
])
.arg(image_url)
]);
// Forward the log filter into the container: the suite policy tests
// assert on tng debug logs, and the container inherits neither the
// parent environment nor the testsuite subscriber.
if let Ok(rust_log) = std::env::var("RUST_LOG") {
cmd.arg("-e").arg(format!("RUST_LOG={rust_log}"));
}
cmd.arg(image_url)
.arg("tng")
.arg("launch")
.arg("--config-content")
Expand Down
Loading
Loading