mirror of
https://github.com/telemt/telemt.git
synced 2026-05-22 19:51:43 +03:00
Compare commits
9 Commits
3.4.11
...
422d97a385
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
422d97a385 | ||
|
|
6b0cc48c2b | ||
|
|
914f141715 | ||
|
|
9e877e45c9 | ||
|
|
0af64a4d0a | ||
|
|
f77e9b8881 | ||
|
|
25ca64de1b | ||
|
|
8895947414 | ||
|
|
7a284623d6 |
@@ -13,7 +13,7 @@ API runtime is configured in `[server.api]`.
|
|||||||
| `listen` | `string` (`IP:PORT`) | `0.0.0.0:9091` | API bind address. |
|
| `listen` | `string` (`IP:PORT`) | `0.0.0.0:9091` | API bind address. |
|
||||||
| `whitelist` | `CIDR[]` | `127.0.0.0/8` | Source IP allowlist. Empty list means allow all. |
|
| `whitelist` | `CIDR[]` | `127.0.0.0/8` | Source IP allowlist. Empty list means allow all. |
|
||||||
| `auth_header` | `string` | `""` | Exact value for `Authorization` header. Empty disables header auth. |
|
| `auth_header` | `string` | `""` | Exact value for `Authorization` header. Empty disables header auth. |
|
||||||
| `request_body_limit_bytes` | `usize` | `65536` | Maximum request body size. Must be `> 0`. |
|
| `request_body_limit_bytes` | `usize` | `65536` | Maximum request body size. Must be within `[1, 1048576]`. |
|
||||||
| `minimal_runtime_enabled` | `bool` | `true` | Enables runtime snapshot endpoints requiring ME pool read-lock aggregation. |
|
| `minimal_runtime_enabled` | `bool` | `true` | Enables runtime snapshot endpoints requiring ME pool read-lock aggregation. |
|
||||||
| `minimal_runtime_cache_ttl_ms` | `u64` | `1000` | Cache TTL for minimal snapshots. `0` disables cache; valid range is `[0, 60000]`. |
|
| `minimal_runtime_cache_ttl_ms` | `u64` | `1000` | Cache TTL for minimal snapshots. `0` disables cache; valid range is `[0, 60000]`. |
|
||||||
| `runtime_edge_enabled` | `bool` | `false` | Enables runtime edge endpoints with cached aggregation payloads. |
|
| `runtime_edge_enabled` | `bool` | `false` | Enables runtime edge endpoints with cached aggregation payloads. |
|
||||||
@@ -26,7 +26,7 @@ API runtime is configured in `[server.api]`.
|
|||||||
|
|
||||||
Runtime validation for API config:
|
Runtime validation for API config:
|
||||||
- `server.api.listen` must be a valid `IP:PORT`.
|
- `server.api.listen` must be a valid `IP:PORT`.
|
||||||
- `server.api.request_body_limit_bytes` must be `> 0`.
|
- `server.api.request_body_limit_bytes` must be within `[1, 1048576]`.
|
||||||
- `server.api.minimal_runtime_cache_ttl_ms` must be within `[0, 60000]`.
|
- `server.api.minimal_runtime_cache_ttl_ms` must be within `[0, 60000]`.
|
||||||
- `server.api.runtime_edge_cache_ttl_ms` must be within `[0, 60000]`.
|
- `server.api.runtime_edge_cache_ttl_ms` must be within `[0, 60000]`.
|
||||||
- `server.api.runtime_edge_top_n` must be within `[1, 1000]`.
|
- `server.api.runtime_edge_top_n` must be within `[1, 1000]`.
|
||||||
@@ -76,13 +76,14 @@ Requests are processed in this order:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Whitelist is evaluated against the direct TCP peer IP (`SocketAddr::ip`), without `X-Forwarded-For` support.
|
- Whitelist is evaluated against the direct TCP peer IP (`SocketAddr::ip`), without `X-Forwarded-For` support.
|
||||||
- `Authorization` check is exact string equality against configured `auth_header`.
|
- `Authorization` check is exact constant-time byte equality against configured `auth_header`.
|
||||||
|
|
||||||
## Endpoint Matrix
|
## Endpoint Matrix
|
||||||
|
|
||||||
| Method | Path | Body | Success | `data` contract |
|
| Method | Path | Body | Success | `data` contract |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `GET` | `/v1/health` | none | `200` | `HealthData` |
|
| `GET` | `/v1/health` | none | `200` | `HealthData` |
|
||||||
|
| `GET` | `/v1/health/ready` | none | `200` or `503` | `HealthReadyData` |
|
||||||
| `GET` | `/v1/system/info` | none | `200` | `SystemInfoData` |
|
| `GET` | `/v1/system/info` | none | `200` | `SystemInfoData` |
|
||||||
| `GET` | `/v1/runtime/gates` | none | `200` | `RuntimeGatesData` |
|
| `GET` | `/v1/runtime/gates` | none | `200` | `RuntimeGatesData` |
|
||||||
| `GET` | `/v1/runtime/initialization` | none | `200` | `RuntimeInitializationData` |
|
| `GET` | `/v1/runtime/initialization` | none | `200` | `RuntimeInitializationData` |
|
||||||
@@ -102,13 +103,50 @@ Notes:
|
|||||||
| `GET` | `/v1/runtime/me-selftest` | none | `200` | `RuntimeMeSelftestData` |
|
| `GET` | `/v1/runtime/me-selftest` | none | `200` | `RuntimeMeSelftestData` |
|
||||||
| `GET` | `/v1/runtime/connections/summary` | none | `200` | `RuntimeEdgeConnectionsSummaryData` |
|
| `GET` | `/v1/runtime/connections/summary` | none | `200` | `RuntimeEdgeConnectionsSummaryData` |
|
||||||
| `GET` | `/v1/runtime/events/recent` | none | `200` | `RuntimeEdgeEventsData` |
|
| `GET` | `/v1/runtime/events/recent` | none | `200` | `RuntimeEdgeEventsData` |
|
||||||
|
| `GET` | `/v1/stats/users/active-ips` | none | `200` | `UserActiveIps[]` |
|
||||||
| `GET` | `/v1/stats/users` | none | `200` | `UserInfo[]` |
|
| `GET` | `/v1/stats/users` | none | `200` | `UserInfo[]` |
|
||||||
| `GET` | `/v1/users` | none | `200` | `UserInfo[]` |
|
| `GET` | `/v1/users` | none | `200` | `UserInfo[]` |
|
||||||
| `POST` | `/v1/users` | `CreateUserRequest` | `201` | `CreateUserResponse` |
|
| `POST` | `/v1/users` | `CreateUserRequest` | `201` or `202` | `CreateUserResponse` |
|
||||||
| `GET` | `/v1/users/{username}` | none | `200` | `UserInfo` |
|
| `GET` | `/v1/users/{username}` | none | `200` | `UserInfo` |
|
||||||
| `PATCH` | `/v1/users/{username}` | `PatchUserRequest` | `200` | `UserInfo` |
|
| `PATCH` | `/v1/users/{username}` | `PatchUserRequest` | `200` or `202` | `UserInfo` |
|
||||||
| `DELETE` | `/v1/users/{username}` | none | `200` | `string` (deleted username) |
|
| `DELETE` | `/v1/users/{username}` | none | `200` or `202` | `DeleteUserResponse` |
|
||||||
| `POST` | `/v1/users/{username}/rotate-secret` | `RotateSecretRequest` or empty body | `404` | `ErrorResponse` (`not_found`, current runtime behavior) |
|
| `POST` | `/v1/users/{username}/rotate-secret` | `RotateSecretRequest` or empty body | `200` or `202` | `CreateUserResponse` |
|
||||||
|
| `POST` | `/v1/users/{username}/reset-quota` | empty body | `200` | `ResetUserQuotaResponse` |
|
||||||
|
|
||||||
|
## Endpoint Behavior
|
||||||
|
|
||||||
|
| Endpoint | Function |
|
||||||
|
| --- | --- |
|
||||||
|
| `GET /v1/health` | Returns basic API liveness and current `read_only` flag. |
|
||||||
|
| `GET /v1/health/ready` | Returns readiness based on admission state and upstream health; returns `503` when not ready. |
|
||||||
|
| `GET /v1/system/info` | Returns binary/build metadata, process uptime, config path/hash, and reload counters. |
|
||||||
|
| `GET /v1/runtime/gates` | Returns admission, ME readiness, fallback/reroute, and startup gate state. |
|
||||||
|
| `GET /v1/runtime/initialization` | Returns startup progress, ME initialization status, and per-component timeline. |
|
||||||
|
| `GET /v1/limits/effective` | Returns effective timeout, upstream, ME, unique-IP, and TCP policy values after config defaults/resolution. |
|
||||||
|
| `GET /v1/security/posture` | Returns current API/security/telemetry posture flags. |
|
||||||
|
| `GET /v1/security/whitelist` | Returns configured API whitelist CIDRs. |
|
||||||
|
| `GET /v1/stats/summary` | Returns compact core counters and classed failure counters. |
|
||||||
|
| `GET /v1/stats/zero/all` | Returns zero-cost core, upstream, ME, pool, and desync counters. |
|
||||||
|
| `GET /v1/stats/upstreams` | Returns upstream zero counters and, when enabled/available, runtime upstream health rows. |
|
||||||
|
| `GET /v1/stats/minimal/all` | Returns cached minimal ME writer/DC/runtime/network-path snapshot. |
|
||||||
|
| `GET /v1/stats/me-writers` | Returns cached ME writer coverage and per-writer status rows. |
|
||||||
|
| `GET /v1/stats/dcs` | Returns cached per-DC endpoint/writer/load status rows. |
|
||||||
|
| `GET /v1/runtime/me_pool_state` | Returns active/warm/pending/draining generation state, writer contour/health, and refill state. |
|
||||||
|
| `GET /v1/runtime/me_quality` | Returns ME lifecycle counters, route-drop counters, family states, drain gate, and per-DC RTT/coverage. |
|
||||||
|
| `GET /v1/runtime/upstream_quality` | Returns upstream policy/counters plus runtime upstream health rows when available. |
|
||||||
|
| `GET /v1/runtime/nat_stun` | Returns NAT/STUN runtime flags, configured/live STUN servers, reflection cache, and backoff. |
|
||||||
|
| `GET /v1/runtime/me-selftest` | Returns ME self-test state for KDF, time skew, IP family, PID, and SOCKS BND observations. |
|
||||||
|
| `GET /v1/runtime/connections/summary` | Returns runtime-edge connection totals and top-N users by connections/throughput. |
|
||||||
|
| `GET /v1/runtime/events/recent` | Returns recent API/runtime event records with optional `limit` query. |
|
||||||
|
| `GET /v1/stats/users/active-ips` | Returns users that currently have non-empty active source-IP lists. |
|
||||||
|
| `GET /v1/stats/users` | Alias of `GET /v1/users`; returns disk-first user views with runtime lag flag. |
|
||||||
|
| `GET /v1/users` | Returns disk-first user views sorted by username. |
|
||||||
|
| `POST /v1/users` | Creates a user and returns the effective user view plus secret. |
|
||||||
|
| `GET /v1/users/{username}` | Returns one disk-first user view or `404` when absent. |
|
||||||
|
| `PATCH /v1/users/{username}` | Updates selected per-user fields with JSON Merge Patch semantics. |
|
||||||
|
| `DELETE /v1/users/{username}` | Deletes one user and related per-user access-map entries. |
|
||||||
|
| `POST /v1/users/{username}/rotate-secret` | Rotates one user's secret and returns the effective secret. |
|
||||||
|
| `POST /v1/users/{username}/reset-quota` | Resets one user's runtime quota counter and persists quota state. |
|
||||||
|
|
||||||
## Common Error Codes
|
## Common Error Codes
|
||||||
|
|
||||||
@@ -118,7 +156,7 @@ Notes:
|
|||||||
| `401` | `unauthorized` | Missing/invalid `Authorization` when `auth_header` is configured. |
|
| `401` | `unauthorized` | Missing/invalid `Authorization` when `auth_header` is configured. |
|
||||||
| `403` | `forbidden` | Source IP is not allowed by whitelist. |
|
| `403` | `forbidden` | Source IP is not allowed by whitelist. |
|
||||||
| `403` | `read_only` | Mutating endpoint called while `read_only=true`. |
|
| `403` | `read_only` | Mutating endpoint called while `read_only=true`. |
|
||||||
| `404` | `not_found` | Unknown route, unknown user, or unsupported sub-route (including current `rotate-secret` route). |
|
| `404` | `not_found` | Unknown route, unknown user, or unsupported sub-route. |
|
||||||
| `405` | `method_not_allowed` | Unsupported method for `/v1/users/{username}` route shape. |
|
| `405` | `method_not_allowed` | Unsupported method for `/v1/users/{username}` route shape. |
|
||||||
| `409` | `revision_conflict` | `If-Match` revision mismatch. |
|
| `409` | `revision_conflict` | `If-Match` revision mismatch. |
|
||||||
| `409` | `user_exists` | User already exists on create. |
|
| `409` | `user_exists` | User already exists on create. |
|
||||||
@@ -132,11 +170,12 @@ Notes:
|
|||||||
| Case | Behavior |
|
| Case | Behavior |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Path matching | Exact match on `req.uri().path()`. Query string does not affect route matching. |
|
| Path matching | Exact match on `req.uri().path()`. Query string does not affect route matching. |
|
||||||
| Trailing slash | Not normalized. Example: `/v1/users/` is `404`. |
|
| Trailing slash | Trimmed for route matching when path length is greater than 1. Example: `/v1/users/` matches `/v1/users`. |
|
||||||
| Username route with extra slash | `/v1/users/{username}/...` is not treated as user route and returns `404`. |
|
| Username route with extra slash | `/v1/users/{username}/...` is not treated as user route and returns `404`. |
|
||||||
| `PUT /v1/users/{username}` | `405 method_not_allowed`. |
|
| `PUT /v1/users/{username}` | `405 method_not_allowed`. |
|
||||||
| `POST /v1/users/{username}` | `404 not_found`. |
|
| `POST /v1/users/{username}` | `404 not_found`. |
|
||||||
| `POST /v1/users/{username}/rotate-secret` | `404 not_found` in current release due route matcher limitation. |
|
| `POST /v1/users/{username}/rotate-secret/` | Trailing slash is trimmed and the route matches `rotate-secret`. |
|
||||||
|
| `POST /v1/users/{username}/reset-quota/` | Trailing slash is trimmed and the route matches `reset-quota`. |
|
||||||
|
|
||||||
## Body and JSON Semantics
|
## Body and JSON Semantics
|
||||||
|
|
||||||
@@ -146,7 +185,7 @@ Notes:
|
|||||||
- Invalid JSON returns `400 bad_request` (`Invalid JSON body`).
|
- Invalid JSON returns `400 bad_request` (`Invalid JSON body`).
|
||||||
- `Content-Type` is not required for JSON parsing.
|
- `Content-Type` is not required for JSON parsing.
|
||||||
- Unknown JSON fields are ignored by deserialization.
|
- Unknown JSON fields are ignored by deserialization.
|
||||||
- `PATCH` updates only provided fields and does not support explicit clearing of optional fields.
|
- `PATCH` uses JSON Merge Patch semantics for optional per-user fields: omitted means unchanged, explicit `null` removes the config entry, and a non-null value sets it.
|
||||||
- `If-Match` supports both quoted and unquoted values; surrounding whitespace is trimmed.
|
- `If-Match` supports both quoted and unquoted values; surrounding whitespace is trimmed.
|
||||||
|
|
||||||
## Query Parameters
|
## Query Parameters
|
||||||
@@ -166,17 +205,21 @@ Notes:
|
|||||||
| `max_tcp_conns` | `usize` | no | Per-user concurrent TCP limit. |
|
| `max_tcp_conns` | `usize` | no | Per-user concurrent TCP limit. |
|
||||||
| `expiration_rfc3339` | `string` | no | RFC3339 expiration timestamp. |
|
| `expiration_rfc3339` | `string` | no | RFC3339 expiration timestamp. |
|
||||||
| `data_quota_bytes` | `u64` | no | Per-user traffic quota. |
|
| `data_quota_bytes` | `u64` | no | Per-user traffic quota. |
|
||||||
|
| `rate_limit_up_bps` | `u64` | no | Per-user upload rate limit in bytes per second. |
|
||||||
|
| `rate_limit_down_bps` | `u64` | no | Per-user download rate limit in bytes per second. |
|
||||||
| `max_unique_ips` | `usize` | no | Per-user unique source IP limit. |
|
| `max_unique_ips` | `usize` | no | Per-user unique source IP limit. |
|
||||||
|
|
||||||
### `PatchUserRequest`
|
### `PatchUserRequest`
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `secret` | `string` | no | Exactly 32 hex chars. |
|
| `secret` | `string` | no | Exactly 32 hex chars. |
|
||||||
| `user_ad_tag` | `string` | no | Exactly 32 hex chars. |
|
| `user_ad_tag` | `string|null` | no | Exactly 32 hex chars; `null` removes the per-user ad tag. |
|
||||||
| `max_tcp_conns` | `usize` | no | Per-user concurrent TCP limit. |
|
| `max_tcp_conns` | `usize|null` | no | Per-user concurrent TCP limit; `null` removes the per-user override. |
|
||||||
| `expiration_rfc3339` | `string` | no | RFC3339 expiration timestamp. |
|
| `expiration_rfc3339` | `string|null` | no | RFC3339 expiration timestamp; `null` removes the expiration. |
|
||||||
| `data_quota_bytes` | `u64` | no | Per-user traffic quota. |
|
| `data_quota_bytes` | `u64|null` | no | Per-user traffic quota; `null` removes the per-user quota. |
|
||||||
| `max_unique_ips` | `usize` | no | Per-user unique source IP limit. |
|
| `rate_limit_up_bps` | `u64|null` | no | Per-user upload rate limit in bytes per second; `null` removes the upload direction limit. |
|
||||||
|
| `rate_limit_down_bps` | `u64|null` | no | Per-user download rate limit in bytes per second; `null` removes the download direction limit. |
|
||||||
|
| `max_unique_ips` | `usize|null` | no | Per-user unique source IP limit; `null` removes the per-user override. |
|
||||||
|
|
||||||
### `access.user_source_deny` via API
|
### `access.user_source_deny` via API
|
||||||
- In current API surface, per-user deny-list is **not** exposed as a dedicated field in `CreateUserRequest` / `PatchUserRequest`.
|
- In current API surface, per-user deny-list is **not** exposed as a dedicated field in `CreateUserRequest` / `PatchUserRequest`.
|
||||||
@@ -198,7 +241,7 @@ bob = ["198.51.100.42/32"]
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `secret` | `string` | no | Exactly 32 hex chars. If missing, generated automatically. |
|
| `secret` | `string` | no | Exactly 32 hex chars. If missing, generated automatically. |
|
||||||
|
|
||||||
Note: the request contract is defined, but the corresponding route currently returns `404` (see routing edge cases).
|
An empty request body is accepted and generates a new secret automatically.
|
||||||
|
|
||||||
## Response Data Contracts
|
## Response Data Contracts
|
||||||
|
|
||||||
@@ -208,15 +251,33 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `status` | `string` | Always `"ok"`. |
|
| `status` | `string` | Always `"ok"`. |
|
||||||
| `read_only` | `bool` | Mirrors current API `read_only` mode. |
|
| `read_only` | `bool` | Mirrors current API `read_only` mode. |
|
||||||
|
|
||||||
|
### `HealthReadyData`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ready` | `bool` | `true` when admission is open and at least one upstream is healthy. |
|
||||||
|
| `status` | `string` | `"ready"` or `"not_ready"`. |
|
||||||
|
| `reason` | `string?` | `admission_closed` or `no_healthy_upstreams` when not ready. |
|
||||||
|
| `admission_open` | `bool` | Current admission-gate state. |
|
||||||
|
| `healthy_upstreams` | `usize` | Number of healthy upstream entries. |
|
||||||
|
| `total_upstreams` | `usize` | Number of configured upstream entries. |
|
||||||
|
|
||||||
### `SummaryData`
|
### `SummaryData`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `uptime_seconds` | `f64` | Process uptime in seconds. |
|
| `uptime_seconds` | `f64` | Process uptime in seconds. |
|
||||||
| `connections_total` | `u64` | Total accepted client connections. |
|
| `connections_total` | `u64` | Total accepted client connections. |
|
||||||
| `connections_bad_total` | `u64` | Failed/invalid client connections. |
|
| `connections_bad_total` | `u64` | Failed/invalid client connections. |
|
||||||
|
| `connections_bad_by_class` | `ClassCount[]` | Failed/invalid connections grouped by class. |
|
||||||
|
| `handshake_failures_by_class` | `ClassCount[]` | Handshake failures grouped by class. |
|
||||||
| `handshake_timeouts_total` | `u64` | Handshake timeout count. |
|
| `handshake_timeouts_total` | `u64` | Handshake timeout count. |
|
||||||
| `configured_users` | `usize` | Number of configured users in config. |
|
| `configured_users` | `usize` | Number of configured users in config. |
|
||||||
|
|
||||||
|
#### `ClassCount`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `class` | `string` | Failure class label. |
|
||||||
|
| `total` | `u64` | Counter value for this class. |
|
||||||
|
|
||||||
### `SystemInfoData`
|
### `SystemInfoData`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -241,7 +302,12 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `conditional_cast_enabled` | `bool` | Whether conditional ME admission logic is enabled (`general.use_middle_proxy`). |
|
| `conditional_cast_enabled` | `bool` | Whether conditional ME admission logic is enabled (`general.use_middle_proxy`). |
|
||||||
| `me_runtime_ready` | `bool` | Current ME runtime readiness status used for conditional gate decisions. |
|
| `me_runtime_ready` | `bool` | Current ME runtime readiness status used for conditional gate decisions. |
|
||||||
| `me2dc_fallback_enabled` | `bool` | Whether ME -> direct fallback is enabled. |
|
| `me2dc_fallback_enabled` | `bool` | Whether ME -> direct fallback is enabled. |
|
||||||
|
| `me2dc_fast_enabled` | `bool` | Whether fast ME -> direct fallback is enabled. |
|
||||||
| `use_middle_proxy` | `bool` | Current transport mode preference. |
|
| `use_middle_proxy` | `bool` | Current transport mode preference. |
|
||||||
|
| `route_mode` | `string` | Current route mode label from route runtime controller. |
|
||||||
|
| `reroute_active` | `bool` | `true` when ME fallback currently routes new sessions to Direct-DC. |
|
||||||
|
| `reroute_to_direct_at_epoch_secs` | `u64?` | Unix timestamp when current direct reroute began. |
|
||||||
|
| `reroute_reason` | `string?` | `startup_direct_fallback`, `fast_not_ready_fallback`, or `strict_grace_fallback` while reroute is active. |
|
||||||
| `startup_status` | `string` | Startup status (`pending`, `initializing`, `ready`, `failed`, `skipped`). |
|
| `startup_status` | `string` | Startup status (`pending`, `initializing`, `ready`, `failed`, `skipped`). |
|
||||||
| `startup_stage` | `string` | Current startup stage identifier. |
|
| `startup_stage` | `string` | Current startup stage identifier. |
|
||||||
| `startup_progress_pct` | `f64` | Startup progress percentage (`0..100`). |
|
| `startup_progress_pct` | `f64` | Startup progress percentage (`0..100`). |
|
||||||
@@ -292,11 +358,13 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `upstream` | `EffectiveUpstreamLimits` | Effective upstream connect/retry limits. |
|
| `upstream` | `EffectiveUpstreamLimits` | Effective upstream connect/retry limits. |
|
||||||
| `middle_proxy` | `EffectiveMiddleProxyLimits` | Effective ME pool/floor/reconnect limits. |
|
| `middle_proxy` | `EffectiveMiddleProxyLimits` | Effective ME pool/floor/reconnect limits. |
|
||||||
| `user_ip_policy` | `EffectiveUserIpPolicyLimits` | Effective unique-IP policy mode/window. |
|
| `user_ip_policy` | `EffectiveUserIpPolicyLimits` | Effective unique-IP policy mode/window. |
|
||||||
|
| `user_tcp_policy` | `EffectiveUserTcpPolicyLimits` | Effective per-user TCP connection policy. |
|
||||||
|
|
||||||
#### `EffectiveTimeoutLimits`
|
#### `EffectiveTimeoutLimits`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `client_handshake_secs` | `u64` | Client handshake timeout. |
|
| `client_handshake_secs` | `u64` | Client handshake timeout. |
|
||||||
|
| `client_first_byte_idle_secs` | `u64` | First-byte idle timeout before protocol classification. |
|
||||||
| `tg_connect_secs` | `u64` | Upstream Telegram connect timeout. |
|
| `tg_connect_secs` | `u64` | Upstream Telegram connect timeout. |
|
||||||
| `client_keepalive_secs` | `u64` | Client keepalive interval. |
|
| `client_keepalive_secs` | `u64` | Client keepalive interval. |
|
||||||
| `client_ack_secs` | `u64` | ACK timeout. |
|
| `client_ack_secs` | `u64` | ACK timeout. |
|
||||||
@@ -335,13 +403,20 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `writer_pick_mode` | `string` | Writer picker mode (`sorted_rr`, `p2c`). |
|
| `writer_pick_mode` | `string` | Writer picker mode (`sorted_rr`, `p2c`). |
|
||||||
| `writer_pick_sample_size` | `u8` | Candidate sample size for `p2c` picker mode. |
|
| `writer_pick_sample_size` | `u8` | Candidate sample size for `p2c` picker mode. |
|
||||||
| `me2dc_fallback` | `bool` | Effective ME -> direct fallback flag. |
|
| `me2dc_fallback` | `bool` | Effective ME -> direct fallback flag. |
|
||||||
|
| `me2dc_fast` | `bool` | Effective fast fallback flag. |
|
||||||
|
|
||||||
#### `EffectiveUserIpPolicyLimits`
|
#### `EffectiveUserIpPolicyLimits`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| `global_each` | `usize` | Global per-user unique-IP limit applied when no per-user override exists. |
|
||||||
| `mode` | `string` | Unique-IP policy mode (`active_window`, `time_window`, `combined`). |
|
| `mode` | `string` | Unique-IP policy mode (`active_window`, `time_window`, `combined`). |
|
||||||
| `window_secs` | `u64` | Time window length used by unique-IP policy. |
|
| `window_secs` | `u64` | Time window length used by unique-IP policy. |
|
||||||
|
|
||||||
|
#### `EffectiveUserTcpPolicyLimits`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `global_each` | `usize` | Global per-user concurrent TCP limit applied when no per-user override exists. |
|
||||||
|
|
||||||
### `SecurityPostureData`
|
### `SecurityPostureData`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -445,6 +520,8 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `counters` | `RuntimeMeQualityCountersData` | Key ME lifecycle/error counters. |
|
| `counters` | `RuntimeMeQualityCountersData` | Key ME lifecycle/error counters. |
|
||||||
| `route_drops` | `RuntimeMeQualityRouteDropData` | Route drop counters by reason. |
|
| `route_drops` | `RuntimeMeQualityRouteDropData` | Route drop counters by reason. |
|
||||||
|
| `family_states` | `RuntimeMeQualityFamilyStateData[]` | Per-family ME route/recovery state rows. |
|
||||||
|
| `drain_gate` | `RuntimeMeQualityDrainGateData` | Current ME drain-gate decision state. |
|
||||||
| `dc_rtt` | `RuntimeMeQualityDcRttData[]` | Per-DC RTT and writer coverage rows. |
|
| `dc_rtt` | `RuntimeMeQualityDcRttData[]` | Per-DC RTT and writer coverage rows. |
|
||||||
|
|
||||||
#### `RuntimeMeQualityCountersData`
|
#### `RuntimeMeQualityCountersData`
|
||||||
@@ -466,6 +543,24 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `queue_full_base_total` | `u64` | Route drops in base-queue path. |
|
| `queue_full_base_total` | `u64` | Route drops in base-queue path. |
|
||||||
| `queue_full_high_total` | `u64` | Route drops in high-priority queue path. |
|
| `queue_full_high_total` | `u64` | Route drops in high-priority queue path. |
|
||||||
|
|
||||||
|
#### `RuntimeMeQualityFamilyStateData`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `family` | `string` | Address family label. |
|
||||||
|
| `state` | `string` | Current family state label. |
|
||||||
|
| `state_since_epoch_secs` | `u64` | Unix timestamp when current state began. |
|
||||||
|
| `suppressed_until_epoch_secs` | `u64?` | Unix timestamp until suppression remains active. |
|
||||||
|
| `fail_streak` | `u32` | Consecutive failure count. |
|
||||||
|
| `recover_success_streak` | `u32` | Consecutive recovery success count. |
|
||||||
|
|
||||||
|
#### `RuntimeMeQualityDrainGateData`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `route_quorum_ok` | `bool` | Whether route quorum condition allows drain. |
|
||||||
|
| `redundancy_ok` | `bool` | Whether redundancy condition allows drain. |
|
||||||
|
| `block_reason` | `string` | Current drain block reason label. |
|
||||||
|
| `updated_at_epoch_secs` | `u64` | Unix timestamp of the latest gate update. |
|
||||||
|
|
||||||
#### `RuntimeMeQualityDcRttData`
|
#### `RuntimeMeQualityDcRttData`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -728,11 +823,24 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `uptime_seconds` | `f64` | Process uptime. |
|
| `uptime_seconds` | `f64` | Process uptime. |
|
||||||
| `connections_total` | `u64` | Total accepted connections. |
|
| `connections_total` | `u64` | Total accepted connections. |
|
||||||
| `connections_bad_total` | `u64` | Failed/invalid connections. |
|
| `connections_bad_total` | `u64` | Failed/invalid connections. |
|
||||||
|
| `connections_bad_by_class` | `ClassCount[]` | Failed/invalid connections grouped by class. |
|
||||||
|
| `handshake_failures_by_class` | `ClassCount[]` | Handshake failures grouped by class. |
|
||||||
| `handshake_timeouts_total` | `u64` | Handshake timeouts. |
|
| `handshake_timeouts_total` | `u64` | Handshake timeouts. |
|
||||||
|
| `accept_permit_timeout_total` | `u64` | Listener admission permit acquisition timeouts. |
|
||||||
| `configured_users` | `usize` | Configured user count. |
|
| `configured_users` | `usize` | Configured user count. |
|
||||||
| `telemetry_core_enabled` | `bool` | Core telemetry toggle. |
|
| `telemetry_core_enabled` | `bool` | Core telemetry toggle. |
|
||||||
| `telemetry_user_enabled` | `bool` | User telemetry toggle. |
|
| `telemetry_user_enabled` | `bool` | User telemetry toggle. |
|
||||||
| `telemetry_me_level` | `string` | ME telemetry level (`off|normal|verbose`). |
|
| `telemetry_me_level` | `string` | ME telemetry level (`off|normal|verbose`). |
|
||||||
|
| `conntrack_control_enabled` | `bool` | Whether conntrack control is enabled by policy. |
|
||||||
|
| `conntrack_control_available` | `bool` | Whether conntrack control backend is currently available. |
|
||||||
|
| `conntrack_pressure_active` | `bool` | Current conntrack pressure flag. |
|
||||||
|
| `conntrack_event_queue_depth` | `u64` | Current conntrack close-event queue depth. |
|
||||||
|
| `conntrack_rule_apply_ok` | `bool` | Last conntrack rule application state. |
|
||||||
|
| `conntrack_delete_attempt_total` | `u64` | Conntrack delete attempts. |
|
||||||
|
| `conntrack_delete_success_total` | `u64` | Successful conntrack deletes. |
|
||||||
|
| `conntrack_delete_not_found_total` | `u64` | Conntrack delete misses. |
|
||||||
|
| `conntrack_delete_error_total` | `u64` | Conntrack delete errors. |
|
||||||
|
| `conntrack_close_event_drop_total` | `u64` | Dropped conntrack close events. |
|
||||||
|
|
||||||
#### `ZeroUpstreamData`
|
#### `ZeroUpstreamData`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
@@ -819,6 +927,24 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `route_drop_queue_full_total` | `u64` | Route drops due to full queue (total). |
|
| `route_drop_queue_full_total` | `u64` | Route drops due to full queue (total). |
|
||||||
| `route_drop_queue_full_base_total` | `u64` | Route drops in base queue mode. |
|
| `route_drop_queue_full_base_total` | `u64` | Route drops in base queue mode. |
|
||||||
| `route_drop_queue_full_high_total` | `u64` | Route drops in high queue mode. |
|
| `route_drop_queue_full_high_total` | `u64` | Route drops in high queue mode. |
|
||||||
|
| `d2c_batches_total` | `u64` | ME D->C batch flushes. |
|
||||||
|
| `d2c_batch_frames_total` | `u64` | ME D->C frames included in batches. |
|
||||||
|
| `d2c_batch_bytes_total` | `u64` | ME D->C payload bytes included in batches. |
|
||||||
|
| `d2c_flush_reason_queue_drain_total` | `u64` | Flushes caused by queue drain. |
|
||||||
|
| `d2c_flush_reason_batch_frames_total` | `u64` | Flushes caused by frame-count batch limit. |
|
||||||
|
| `d2c_flush_reason_batch_bytes_total` | `u64` | Flushes caused by byte-count batch limit. |
|
||||||
|
| `d2c_flush_reason_max_delay_total` | `u64` | Flushes caused by max-delay budget. |
|
||||||
|
| `d2c_flush_reason_ack_immediate_total` | `u64` | Flushes caused by immediate ACK policy. |
|
||||||
|
| `d2c_flush_reason_close_total` | `u64` | Flushes caused by close path. |
|
||||||
|
| `d2c_data_frames_total` | `u64` | ME D->C data frames. |
|
||||||
|
| `d2c_ack_frames_total` | `u64` | ME D->C ACK frames. |
|
||||||
|
| `d2c_payload_bytes_total` | `u64` | ME D->C payload bytes. |
|
||||||
|
| `d2c_write_mode_coalesced_total` | `u64` | Coalesced D->C writes. |
|
||||||
|
| `d2c_write_mode_split_total` | `u64` | Split D->C writes. |
|
||||||
|
| `d2c_quota_reject_pre_write_total` | `u64` | D->C quota rejects before write. |
|
||||||
|
| `d2c_quota_reject_post_write_total` | `u64` | D->C quota rejects after write. |
|
||||||
|
| `d2c_frame_buf_shrink_total` | `u64` | D->C frame-buffer shrink operations. |
|
||||||
|
| `d2c_frame_buf_shrink_bytes_total` | `u64` | Bytes released by D->C frame-buffer shrink operations. |
|
||||||
| `socks_kdf_strict_reject_total` | `u64` | SOCKS KDF strict rejects. |
|
| `socks_kdf_strict_reject_total` | `u64` | SOCKS KDF strict rejects. |
|
||||||
| `socks_kdf_compat_fallback_total` | `u64` | SOCKS KDF compat fallbacks. |
|
| `socks_kdf_compat_fallback_total` | `u64` | SOCKS KDF compat fallbacks. |
|
||||||
| `endpoint_quarantine_total` | `u64` | Endpoint quarantine activations. |
|
| `endpoint_quarantine_total` | `u64` | Endpoint quarantine activations. |
|
||||||
@@ -978,6 +1104,8 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `required_writers` | `usize` | Required writers based on current floor policy. |
|
| `required_writers` | `usize` | Required writers based on current floor policy. |
|
||||||
| `alive_writers` | `usize` | Writers currently alive. |
|
| `alive_writers` | `usize` | Writers currently alive. |
|
||||||
| `coverage_pct` | `f64` | `alive_writers / required_writers * 100`. |
|
| `coverage_pct` | `f64` | `alive_writers / required_writers * 100`. |
|
||||||
|
| `fresh_alive_writers` | `usize` | Alive writers that match freshness requirements. |
|
||||||
|
| `fresh_coverage_pct` | `f64` | `fresh_alive_writers / required_writers * 100`. |
|
||||||
|
|
||||||
#### `MeWriterStatus`
|
#### `MeWriterStatus`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
@@ -992,6 +1120,12 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `bound_clients` | `usize` | Number of currently bound clients. |
|
| `bound_clients` | `usize` | Number of currently bound clients. |
|
||||||
| `idle_for_secs` | `u64?` | Idle age in seconds if idle. |
|
| `idle_for_secs` | `u64?` | Idle age in seconds if idle. |
|
||||||
| `rtt_ema_ms` | `f64?` | RTT exponential moving average. |
|
| `rtt_ema_ms` | `f64?` | RTT exponential moving average. |
|
||||||
|
| `matches_active_generation` | `bool` | Whether this writer belongs to the active pool generation. |
|
||||||
|
| `in_desired_map` | `bool` | Whether this writer's endpoint remains in desired topology. |
|
||||||
|
| `allow_drain_fallback` | `bool` | Whether drain fallback is allowed for this writer. |
|
||||||
|
| `drain_started_at_epoch_secs` | `u64?` | Unix timestamp when drain started. |
|
||||||
|
| `drain_deadline_epoch_secs` | `u64?` | Unix timestamp of drain deadline. |
|
||||||
|
| `drain_over_ttl` | `bool` | Whether drain has exceeded its TTL. |
|
||||||
|
|
||||||
### `DcStatusData`
|
### `DcStatusData`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
@@ -1016,6 +1150,8 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `floor_capped` | `bool` | `true` when computed floor target was capped by active limits. |
|
| `floor_capped` | `bool` | `true` when computed floor target was capped by active limits. |
|
||||||
| `alive_writers` | `usize` | Alive writers in this DC. |
|
| `alive_writers` | `usize` | Alive writers in this DC. |
|
||||||
| `coverage_pct` | `f64` | `alive_writers / required_writers * 100`. |
|
| `coverage_pct` | `f64` | `alive_writers / required_writers * 100`. |
|
||||||
|
| `fresh_alive_writers` | `usize` | Fresh alive writers in this DC. |
|
||||||
|
| `fresh_coverage_pct` | `f64` | `fresh_alive_writers / required_writers * 100`. |
|
||||||
| `rtt_ms` | `f64?` | Aggregated RTT for DC. |
|
| `rtt_ms` | `f64?` | Aggregated RTT for DC. |
|
||||||
| `load` | `usize` | Active client sessions bound to this DC. |
|
| `load` | `usize` | Active client sessions bound to this DC. |
|
||||||
|
|
||||||
@@ -1029,10 +1165,13 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `username` | `string` | Username. |
|
| `username` | `string` | Username. |
|
||||||
|
| `in_runtime` | `bool` | Whether current runtime config already contains this user. |
|
||||||
| `user_ad_tag` | `string?` | Optional ad tag (32 hex chars). |
|
| `user_ad_tag` | `string?` | Optional ad tag (32 hex chars). |
|
||||||
| `max_tcp_conns` | `usize?` | Optional max concurrent TCP limit. |
|
| `max_tcp_conns` | `usize?` | Optional max concurrent TCP limit. |
|
||||||
| `expiration_rfc3339` | `string?` | Optional expiration timestamp. |
|
| `expiration_rfc3339` | `string?` | Optional expiration timestamp. |
|
||||||
| `data_quota_bytes` | `u64?` | Optional data quota. |
|
| `data_quota_bytes` | `u64?` | Optional data quota. |
|
||||||
|
| `rate_limit_up_bps` | `u64?` | Optional upload rate limit in bytes per second. |
|
||||||
|
| `rate_limit_down_bps` | `u64?` | Optional download rate limit in bytes per second. |
|
||||||
| `max_unique_ips` | `usize?` | Optional unique IP limit. |
|
| `max_unique_ips` | `usize?` | Optional unique IP limit. |
|
||||||
| `current_connections` | `u64` | Current live connections. |
|
| `current_connections` | `u64` | Current live connections. |
|
||||||
| `active_unique_ips` | `usize` | Current active unique source IPs. |
|
| `active_unique_ips` | `usize` | Current active unique source IPs. |
|
||||||
@@ -1042,12 +1181,25 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `total_octets` | `u64` | Total traffic octets for this user. |
|
| `total_octets` | `u64` | Total traffic octets for this user. |
|
||||||
| `links` | `UserLinks` | Active connection links derived from current config. |
|
| `links` | `UserLinks` | Active connection links derived from current config. |
|
||||||
|
|
||||||
|
### `UserActiveIps`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `username` | `string` | Username with at least one active tracked source IP. |
|
||||||
|
| `active_ips` | `ip[]` | Active source IPs for this user. |
|
||||||
|
|
||||||
#### `UserLinks`
|
#### `UserLinks`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `classic` | `string[]` | Active `tg://proxy` links for classic mode. |
|
| `classic` | `string[]` | Active `tg://proxy` links for classic mode. |
|
||||||
| `secure` | `string[]` | Active `tg://proxy` links for secure/DD mode. |
|
| `secure` | `string[]` | Active `tg://proxy` links for secure/DD mode. |
|
||||||
| `tls` | `string[]` | Active `tg://proxy` links for EE-TLS mode (for each host+TLS domain). |
|
| `tls` | `string[]` | Active `tg://proxy` links for EE-TLS mode (for each host+TLS domain). |
|
||||||
|
| `tls_domains` | `TlsDomainLink[]` | Extra TLS-domain links as explicit domain/link pairs for `censorship.tls_domains`. |
|
||||||
|
|
||||||
|
#### `TlsDomainLink`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `domain` | `string` | TLS domain represented by the link. |
|
||||||
|
| `link` | `string` | `tg://proxy` link for this domain. |
|
||||||
|
|
||||||
Link generation uses active config and enabled modes:
|
Link generation uses active config and enabled modes:
|
||||||
- Link port is `general.links.public_port` when configured; otherwise `server.port`.
|
- Link port is `general.links.public_port` when configured; otherwise `server.port`.
|
||||||
@@ -1067,13 +1219,27 @@ Link generation uses active config and enabled modes:
|
|||||||
| `user` | `UserInfo` | Created or updated user view. |
|
| `user` | `UserInfo` | Created or updated user view. |
|
||||||
| `secret` | `string` | Effective user secret. |
|
| `secret` | `string` | Effective user secret. |
|
||||||
|
|
||||||
|
### `DeleteUserResponse`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `username` | `string` | Deleted username. |
|
||||||
|
| `in_runtime` | `bool` | `true` when runtime config still contains the user and hot-reload has not applied deletion yet. |
|
||||||
|
|
||||||
|
### `ResetUserQuotaResponse`
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `username` | `string` | User whose runtime quota counter was reset. |
|
||||||
|
| `used_bytes` | `u64` | Current used bytes after reset; always `0` on success. |
|
||||||
|
| `last_reset_epoch_secs` | `u64` | Unix timestamp of the reset operation. |
|
||||||
|
|
||||||
## Mutation Semantics
|
## Mutation Semantics
|
||||||
|
|
||||||
| Endpoint | Notes |
|
| Endpoint | Notes |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `POST /v1/users` | Creates user, validates config, then atomically updates only affected `access.*` TOML tables (`access.users` always, plus optional per-user tables present in request). |
|
| `POST /v1/users` | Creates user, validates config, then atomically updates only affected `access.*` TOML tables (`access.users` always, plus optional per-user tables present in request). |
|
||||||
| `PATCH /v1/users/{username}` | Partial update of provided fields only. Missing fields remain unchanged. Current implementation persists full config document on success. |
|
| `PATCH /v1/users/{username}` | Partial update of provided fields only. Missing fields remain unchanged; explicit `null` removes optional per-user entries. The write path updates only affected `access.*` TOML tables. |
|
||||||
| `POST /v1/users/{username}/rotate-secret` | Currently returns `404` in runtime route matcher; request schema is reserved for intended behavior. |
|
| `POST /v1/users/{username}/rotate-secret` | Replaces the user's secret with a provided valid 32-hex value or a generated value, then returns the effective secret in `CreateUserResponse`. |
|
||||||
|
| `POST /v1/users/{username}/reset-quota` | Resets the runtime quota counter for the route username, persists quota state to `general.quota_state_path`, and does not modify user config. |
|
||||||
| `DELETE /v1/users/{username}` | Deletes only specified user, removes this user from related optional `access.user_*` maps, blocks last-user deletion, and atomically updates only related `access.*` TOML tables. |
|
| `DELETE /v1/users/{username}` | Deletes only specified user, removes this user from related optional `access.user_*` maps, blocks last-user deletion, and atomically updates only related `access.*` TOML tables. |
|
||||||
|
|
||||||
All mutating endpoints:
|
All mutating endpoints:
|
||||||
@@ -1082,6 +1248,12 @@ All mutating endpoints:
|
|||||||
- Return new `revision` after successful write.
|
- Return new `revision` after successful write.
|
||||||
- Use process-local mutation lock + atomic write (`tmp + rename`) for config persistence.
|
- Use process-local mutation lock + atomic write (`tmp + rename`) for config persistence.
|
||||||
|
|
||||||
|
Docker deployment note:
|
||||||
|
- Mutating endpoints require `config.toml` to live inside a writable mounted directory.
|
||||||
|
- Do not mount `config.toml` as a single bind-mounted file when API mutations are enabled; atomic `tmp + rename` writes can fail with `Device or resource busy`.
|
||||||
|
- Mount the config directory instead, for example `./config:/etc/telemt:rw`, and start Telemt with `/etc/telemt/config.toml`.
|
||||||
|
- A read-only single-file mount remains valid only for read-only deployments or when `[server.api].read_only=true`.
|
||||||
|
|
||||||
Delete path cleanup guarantees:
|
Delete path cleanup guarantees:
|
||||||
- Config cleanup removes only the requested username keys.
|
- Config cleanup removes only the requested username keys.
|
||||||
- Runtime unique-IP cleanup removes only this user's limiter and tracked IP state.
|
- Runtime unique-IP cleanup removes only this user's limiter and tracked IP state.
|
||||||
@@ -1114,12 +1286,12 @@ Additional runtime endpoint behavior:
|
|||||||
## ME Fallback Behavior Exposed Via API
|
## ME Fallback Behavior Exposed Via API
|
||||||
|
|
||||||
When `general.use_middle_proxy=true` and `general.me2dc_fallback=true`:
|
When `general.use_middle_proxy=true` and `general.me2dc_fallback=true`:
|
||||||
- Startup does not block on full ME pool readiness; initialization can continue in background.
|
- Startup opens Direct-DC routing first, then initializes ME in background and switches new sessions to Middle mode after ME readiness is observed.
|
||||||
- Runtime initialization payload can expose ME stage `background_init` until pool becomes ready.
|
- Runtime initialization payload can expose ME stage `background_init` until pool becomes ready.
|
||||||
- Admission/routing decision uses two readiness grace windows for "ME not ready" periods:
|
- Admission/routing decision uses two readiness grace windows for "ME not ready" periods:
|
||||||
`80s` before first-ever readiness is observed (startup grace),
|
direct startup fallback before first-ever readiness is observed,
|
||||||
`6s` after readiness has been observed at least once (runtime failover timeout).
|
`6s` after readiness has been observed at least once (runtime failover timeout).
|
||||||
- While in fallback window breach, new sessions are routed via Direct-DC; when ME becomes ready, routing returns to Middle mode for new sessions.
|
- While fallback is active, new sessions are routed via Direct-DC; when ME becomes ready, routing returns to Middle mode. Direct sessions affected by the cutover are closed with the existing staggered delay so clients reconnect through the current route.
|
||||||
|
|
||||||
## Serialization Rules
|
## Serialization Rules
|
||||||
|
|
||||||
@@ -1148,5 +1320,4 @@ When `general.use_middle_proxy=true` and `general.me2dc_fallback=true`:
|
|||||||
|
|
||||||
## Known Limitations (Current Release)
|
## Known Limitations (Current Release)
|
||||||
|
|
||||||
- `POST /v1/users/{username}/rotate-secret` is currently unreachable in route matcher and returns `404`.
|
|
||||||
- API runtime controls under `server.api` are documented as restart-required; hot-reload behavior for these fields is not strictly uniform in all change combinations.
|
- API runtime controls under `server.api` are documented as restart-required; hot-reload behavior for these fields is not strictly uniform in all change combinations.
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
| [`middle_proxy_warm_standby`](#middle_proxy_warm_standby) | `usize` | `16` |
|
| [`middle_proxy_warm_standby`](#middle_proxy_warm_standby) | `usize` | `16` |
|
||||||
| [`me_init_retry_attempts`](#me_init_retry_attempts) | `u32` | `0` |
|
| [`me_init_retry_attempts`](#me_init_retry_attempts) | `u32` | `0` |
|
||||||
| [`me2dc_fallback`](#me2dc_fallback) | `bool` | `true` |
|
| [`me2dc_fallback`](#me2dc_fallback) | `bool` | `true` |
|
||||||
| [`me2dc_fast`](#me2dc_fast) | `bool` | `false` |
|
| [`me2dc_fast`](#me2dc_fast) | `bool` | `true` |
|
||||||
| [`me_keepalive_enabled`](#me_keepalive_enabled) | `bool` | `true` |
|
| [`me_keepalive_enabled`](#me_keepalive_enabled) | `bool` | `true` |
|
||||||
| [`me_keepalive_interval_secs`](#me_keepalive_interval_secs) | `u64` | `8` |
|
| [`me_keepalive_interval_secs`](#me_keepalive_interval_secs) | `u64` | `8` |
|
||||||
| [`me_keepalive_jitter_secs`](#me_keepalive_jitter_secs) | `u64` | `2` |
|
| [`me_keepalive_jitter_secs`](#me_keepalive_jitter_secs) | `u64` | `2` |
|
||||||
@@ -392,7 +392,7 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
```
|
```
|
||||||
## me2dc_fallback
|
## me2dc_fallback
|
||||||
- **Constraints / validation**: `bool`.
|
- **Constraints / validation**: `bool`.
|
||||||
- **Description**: Allows fallback from ME mode to direct DC when ME startup fails.
|
- **Description**: Allows Direct-DC fallback when ME is unavailable. With `use_middle_proxy = true`, startup opens Direct-DC routing first and moves new sessions to ME after ME readiness is observed.
|
||||||
- **Example**:
|
- **Example**:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -401,14 +401,14 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
```
|
```
|
||||||
## me2dc_fast
|
## me2dc_fast
|
||||||
- **Constraints / validation**: `bool`. Active only when `use_middle_proxy = true` and `me2dc_fallback = true`.
|
- **Constraints / validation**: `bool`. Active only when `use_middle_proxy = true` and `me2dc_fallback = true`.
|
||||||
- **Description**: Fast ME->Direct fallback mode for new sessions.
|
- **Description**: Fast ME->Direct fallback mode for new sessions after ME was ready at least once. Initial direct-first startup fallback is controlled by `me2dc_fallback`.
|
||||||
- **Example**:
|
- **Example**:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
use_middle_proxy = true
|
use_middle_proxy = true
|
||||||
me2dc_fallback = true
|
me2dc_fallback = true
|
||||||
me2dc_fast = false
|
me2dc_fast = true
|
||||||
```
|
```
|
||||||
## me_keepalive_enabled
|
## me_keepalive_enabled
|
||||||
- **Constraints / validation**: `bool`.
|
- **Constraints / validation**: `bool`.
|
||||||
@@ -2352,6 +2352,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
|
|||||||
| [`mask`](#mask) | `bool` | `true` |
|
| [`mask`](#mask) | `bool` | `true` |
|
||||||
| [`mask_host`](#mask_host) | `String` | — |
|
| [`mask_host`](#mask_host) | `String` | — |
|
||||||
| [`mask_port`](#mask_port) | `u16` | `443` |
|
| [`mask_port`](#mask_port) | `u16` | `443` |
|
||||||
|
| [`exclusive_mask`](#exclusive_mask) | `Map<String,String>` | `{}` |
|
||||||
| [`mask_unix_sock`](#mask_unix_sock) | `String` | — |
|
| [`mask_unix_sock`](#mask_unix_sock) | `String` | — |
|
||||||
| [`fake_cert_len`](#fake_cert_len) | `usize` | `2048` |
|
| [`fake_cert_len`](#fake_cert_len) | `usize` | `2048` |
|
||||||
| [`tls_emulation`](#tls_emulation) | `bool` | `true` |
|
| [`tls_emulation`](#tls_emulation) | `bool` | `true` |
|
||||||
@@ -2459,6 +2460,18 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
|
|||||||
[censorship]
|
[censorship]
|
||||||
mask_port = 443
|
mask_port = 443
|
||||||
```
|
```
|
||||||
|
## exclusive_mask
|
||||||
|
- **Constraints / validation**: TOML map. Keys must be SNI domain names. Values must be `host:port` with `port > 0`; IPv6 literals must be bracketed.
|
||||||
|
- **Description**: Per-SNI TCP mask targets for fallback traffic. When a TLS ClientHello SNI matches a key, Telemt relays that unauthenticated connection to the mapped target. Other fallback traffic keeps using the existing `mask_host`/`mask_port` or SNI-aware default masking behavior.
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[censorship]
|
||||||
|
tls_domains = ["petrovich.ru", "bsi.bund.de", "telekom.com"]
|
||||||
|
|
||||||
|
[censorship.exclusive_mask]
|
||||||
|
"bsi.bund.de" = "127.0.0.1:443"
|
||||||
|
```
|
||||||
## mask_unix_sock
|
## mask_unix_sock
|
||||||
- **Constraints / validation**: `String` (optional).
|
- **Constraints / validation**: `String` (optional).
|
||||||
- Must not be empty when set.
|
- Must not be empty when set.
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
| [`middle_proxy_warm_standby`](#middle_proxy_warm_standby) | `usize` | `16` |
|
| [`middle_proxy_warm_standby`](#middle_proxy_warm_standby) | `usize` | `16` |
|
||||||
| [`me_init_retry_attempts`](#me_init_retry_attempts) | `u32` | `0` |
|
| [`me_init_retry_attempts`](#me_init_retry_attempts) | `u32` | `0` |
|
||||||
| [`me2dc_fallback`](#me2dc_fallback) | `bool` | `true` |
|
| [`me2dc_fallback`](#me2dc_fallback) | `bool` | `true` |
|
||||||
| [`me2dc_fast`](#me2dc_fast) | `bool` | `false` |
|
| [`me2dc_fast`](#me2dc_fast) | `bool` | `true` |
|
||||||
| [`me_keepalive_enabled`](#me_keepalive_enabled) | `bool` | `true` |
|
| [`me_keepalive_enabled`](#me_keepalive_enabled) | `bool` | `true` |
|
||||||
| [`me_keepalive_interval_secs`](#me_keepalive_interval_secs) | `u64` | `8` |
|
| [`me_keepalive_interval_secs`](#me_keepalive_interval_secs) | `u64` | `8` |
|
||||||
| [`me_keepalive_jitter_secs`](#me_keepalive_jitter_secs) | `u64` | `2` |
|
| [`me_keepalive_jitter_secs`](#me_keepalive_jitter_secs) | `u64` | `2` |
|
||||||
@@ -392,7 +392,7 @@
|
|||||||
```
|
```
|
||||||
## me2dc_fallback
|
## me2dc_fallback
|
||||||
- **Ограничения / валидация**: `bool`.
|
- **Ограничения / валидация**: `bool`.
|
||||||
- **Описание**: Перейти из режима ME в режим прямого соединения (DC) в случае сбоя запуска ME.
|
- **Описание**: Разрешает fallback на прямой DC, когда ME недоступен. При `use_middle_proxy = true` запуск сначала открывает маршрутизацию через Direct-DC, а новые сеансы переводятся на ME после подтверждения готовности ME.
|
||||||
- **Пример**:
|
- **Пример**:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -401,14 +401,14 @@
|
|||||||
```
|
```
|
||||||
## me2dc_fast
|
## me2dc_fast
|
||||||
- **Ограничения / валидация**: `bool`. Используется только, когда `use_middle_proxy = true` и `me2dc_fallback = true`.
|
- **Ограничения / валидация**: `bool`. Используется только, когда `use_middle_proxy = true` и `me2dc_fallback = true`.
|
||||||
- **Описание**: Режим для быстрого перехода между режимами ME->DC для новых сеансов.
|
- **Описание**: Быстрый fallback ME->Direct для новых сеансов после того, как ME уже был готов хотя бы один раз. Начальный direct-first fallback управляется `me2dc_fallback`.
|
||||||
- **Пример**:
|
- **Пример**:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
use_middle_proxy = true
|
use_middle_proxy = true
|
||||||
me2dc_fallback = true
|
me2dc_fallback = true
|
||||||
me2dc_fast = false
|
me2dc_fast = true
|
||||||
```
|
```
|
||||||
## me_keepalive_enabled
|
## me_keepalive_enabled
|
||||||
- **Ограничения / валидация**: `bool`.
|
- **Ограничения / валидация**: `bool`.
|
||||||
@@ -2358,6 +2358,7 @@
|
|||||||
| [`mask`](#mask) | `bool` | `true` |
|
| [`mask`](#mask) | `bool` | `true` |
|
||||||
| [`mask_host`](#mask_host) | `String` | — |
|
| [`mask_host`](#mask_host) | `String` | — |
|
||||||
| [`mask_port`](#mask_port) | `u16` | `443` |
|
| [`mask_port`](#mask_port) | `u16` | `443` |
|
||||||
|
| [`exclusive_mask`](#exclusive_mask) | `Map<String,String>` | `{}` |
|
||||||
| [`mask_unix_sock`](#mask_unix_sock) | `String` | — |
|
| [`mask_unix_sock`](#mask_unix_sock) | `String` | — |
|
||||||
| [`fake_cert_len`](#fake_cert_len) | `usize` | `2048` |
|
| [`fake_cert_len`](#fake_cert_len) | `usize` | `2048` |
|
||||||
| [`tls_emulation`](#tls_emulation) | `bool` | `true` |
|
| [`tls_emulation`](#tls_emulation) | `bool` | `true` |
|
||||||
@@ -2464,6 +2465,18 @@
|
|||||||
[censorship]
|
[censorship]
|
||||||
mask_port = 443
|
mask_port = 443
|
||||||
```
|
```
|
||||||
|
## exclusive_mask
|
||||||
|
- **Ограничения / валидация**: TOML map. Ключи должны быть доменами SNI. Значения должны иметь формат `host:port`, где `port > 0`; IPv6 literals должны быть в квадратных скобках.
|
||||||
|
- **Описание**: Per-SNI TCP targets для fallback-трафика. Если SNI в TLS ClientHello совпадает с ключом, Telemt проксирует это неаутентифицированное соединение на указанный target. Остальной fallback-трафик продолжает использовать существующий `mask_host`/`mask_port` или SNI-aware default masking behavior.
|
||||||
|
- **Пример**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[censorship]
|
||||||
|
tls_domains = ["petrovich.ru", "bsi.bund.de", "telekom.com"]
|
||||||
|
|
||||||
|
[censorship.exclusive_mask]
|
||||||
|
"bsi.bund.de" = "127.0.0.1:443"
|
||||||
|
```
|
||||||
## mask_unix_sock
|
## mask_unix_sock
|
||||||
- **Ограничения / валидация**: `String` (optional).
|
- **Ограничения / валидация**: `String` (optional).
|
||||||
- Значение не должно быть пустым, если задан.
|
- Значение не должно быть пустым, если задан.
|
||||||
|
|||||||
@@ -254,6 +254,19 @@ docker compose down
|
|||||||
> - `docker-compose.yml` maps `./config.toml` to `/app/config.toml` (read-only)
|
> - `docker-compose.yml` maps `./config.toml` to `/app/config.toml` (read-only)
|
||||||
> - By default it publishes `443:443` and runs with dropped capabilities (only `NET_BIND_SERVICE` is added)
|
> - By default it publishes `443:443` and runs with dropped capabilities (only `NET_BIND_SERVICE` is added)
|
||||||
> - If you really need host networking (usually only for some IPv6 setups) uncomment `network_mode: host`
|
> - If you really need host networking (usually only for some IPv6 setups) uncomment `network_mode: host`
|
||||||
|
> - If you enable mutating Control API endpoints, mount a writable config directory instead of a single `config.toml` file. Telemt persists config changes with atomic `tmp + rename` writes, and a single bind-mounted file can fail with `Device or resource busy`.
|
||||||
|
|
||||||
|
Example writable config mount for Control API mutations:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
telemt:
|
||||||
|
working_dir: /run/telemt
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/telemt:rw
|
||||||
|
tmpfs:
|
||||||
|
- /run/telemt:rw,mode=1777,size=4m
|
||||||
|
command: /usr/local/bin/telemt /etc/telemt/config.toml
|
||||||
|
```
|
||||||
|
|
||||||
**Run without Compose**
|
**Run without Compose**
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use hyper::header::IF_MATCH;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::{ProxyConfig, RateLimitBps};
|
||||||
|
|
||||||
use super::model::ApiFailure;
|
use super::model::ApiFailure;
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ pub(super) enum AccessSection {
|
|||||||
UserMaxTcpConns,
|
UserMaxTcpConns,
|
||||||
UserExpirations,
|
UserExpirations,
|
||||||
UserDataQuota,
|
UserDataQuota,
|
||||||
|
UserRateLimits,
|
||||||
UserMaxUniqueIps,
|
UserMaxUniqueIps,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ impl AccessSection {
|
|||||||
Self::UserMaxTcpConns => "access.user_max_tcp_conns",
|
Self::UserMaxTcpConns => "access.user_max_tcp_conns",
|
||||||
Self::UserExpirations => "access.user_expirations",
|
Self::UserExpirations => "access.user_expirations",
|
||||||
Self::UserDataQuota => "access.user_data_quota",
|
Self::UserDataQuota => "access.user_data_quota",
|
||||||
|
Self::UserRateLimits => "access.user_rate_limits",
|
||||||
Self::UserMaxUniqueIps => "access.user_max_unique_ips",
|
Self::UserMaxUniqueIps => "access.user_max_unique_ips",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +171,15 @@ fn render_access_section(cfg: &ProxyConfig, section: AccessSection) -> Result<St
|
|||||||
.collect();
|
.collect();
|
||||||
serialize_table_body(&rows)?
|
serialize_table_body(&rows)?
|
||||||
}
|
}
|
||||||
|
AccessSection::UserRateLimits => {
|
||||||
|
let rows: BTreeMap<String, RateLimitBps> = cfg
|
||||||
|
.access
|
||||||
|
.user_rate_limits
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| (key.clone(), *value))
|
||||||
|
.collect();
|
||||||
|
serialize_rate_limit_body(&rows)?
|
||||||
|
}
|
||||||
AccessSection::UserMaxUniqueIps => {
|
AccessSection::UserMaxUniqueIps => {
|
||||||
let rows: BTreeMap<String, usize> = cfg
|
let rows: BTreeMap<String, usize> = cfg
|
||||||
.access
|
.access
|
||||||
@@ -197,6 +208,7 @@ fn access_section_is_empty(cfg: &ProxyConfig, section: AccessSection) -> bool {
|
|||||||
AccessSection::UserMaxTcpConns => cfg.access.user_max_tcp_conns.is_empty(),
|
AccessSection::UserMaxTcpConns => cfg.access.user_max_tcp_conns.is_empty(),
|
||||||
AccessSection::UserExpirations => cfg.access.user_expirations.is_empty(),
|
AccessSection::UserExpirations => cfg.access.user_expirations.is_empty(),
|
||||||
AccessSection::UserDataQuota => cfg.access.user_data_quota.is_empty(),
|
AccessSection::UserDataQuota => cfg.access.user_data_quota.is_empty(),
|
||||||
|
AccessSection::UserRateLimits => cfg.access.user_rate_limits.is_empty(),
|
||||||
AccessSection::UserMaxUniqueIps => cfg.access.user_max_unique_ips.is_empty(),
|
AccessSection::UserMaxUniqueIps => cfg.access.user_max_unique_ips.is_empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,6 +218,28 @@ fn serialize_table_body<T: Serialize>(value: &T) -> Result<String, ApiFailure> {
|
|||||||
.map_err(|e| ApiFailure::internal(format!("failed to serialize access section: {}", e)))
|
.map_err(|e| ApiFailure::internal(format!("failed to serialize access section: {}", e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_rate_limit_body(rows: &BTreeMap<String, RateLimitBps>) -> Result<String, ApiFailure> {
|
||||||
|
let mut out = String::new();
|
||||||
|
for (key, value) in rows {
|
||||||
|
let key = serialize_toml_key(key)?;
|
||||||
|
out.push_str(&format!(
|
||||||
|
"{key} = {{ up_bps = {}, down_bps = {} }}\n",
|
||||||
|
value.up_bps, value.down_bps
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_toml_key(key: &str) -> Result<String, ApiFailure> {
|
||||||
|
let mut row = BTreeMap::new();
|
||||||
|
row.insert(key.to_string(), 0_u8);
|
||||||
|
let rendered = serialize_table_body(&row)?;
|
||||||
|
rendered
|
||||||
|
.split_once(" = ")
|
||||||
|
.map(|(key, _)| key.to_string())
|
||||||
|
.ok_or_else(|| ApiFailure::internal("failed to serialize TOML key"))
|
||||||
|
}
|
||||||
|
|
||||||
fn upsert_toml_table(source: &str, table_name: &str, replacement: &str) -> String {
|
fn upsert_toml_table(source: &str, table_name: &str, replacement: &str) -> String {
|
||||||
if let Some((start, end)) = find_toml_table_bounds(source, table_name) {
|
if let Some((start, end)) = find_toml_table_bounds(source, table_name) {
|
||||||
let mut out = String::with_capacity(source.len() + replacement.len());
|
let mut out = String::with_capacity(source.len() + replacement.len());
|
||||||
@@ -285,3 +319,26 @@ fn write_atomic_sync(path: &Path, contents: &str) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
write_result
|
write_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_user_rate_limits_section() {
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.access.user_rate_limits.insert(
|
||||||
|
"alice".to_string(),
|
||||||
|
RateLimitBps {
|
||||||
|
up_bps: 1024,
|
||||||
|
down_bps: 2048,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let rendered = render_access_section(&cfg, AccessSection::UserRateLimits)
|
||||||
|
.expect("section must render");
|
||||||
|
|
||||||
|
assert!(rendered.starts_with("[access.user_rate_limits]\n"));
|
||||||
|
assert!(rendered.contains("alice = { up_bps = 1024, down_bps = 2048 }"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ use runtime_zero::{
|
|||||||
build_limits_effective_data, build_runtime_gates_data, build_security_posture_data,
|
build_limits_effective_data, build_runtime_gates_data, build_security_posture_data,
|
||||||
build_system_info_data,
|
build_system_info_data,
|
||||||
};
|
};
|
||||||
use users::{create_user, delete_user, patch_user, rotate_secret, users_from_config};
|
use users::{
|
||||||
|
build_user_quota_list, create_user, delete_user, patch_user, rotate_secret, users_from_config,
|
||||||
|
};
|
||||||
|
|
||||||
const API_MAX_CONTROL_CONNECTIONS: usize = 1024;
|
const API_MAX_CONTROL_CONNECTIONS: usize = 1024;
|
||||||
const API_HTTP_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15);
|
const API_HTTP_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15);
|
||||||
@@ -504,6 +506,12 @@ async fn handle(
|
|||||||
.await;
|
.await;
|
||||||
Ok(success_response(StatusCode::OK, users, revision))
|
Ok(success_response(StatusCode::OK, users, revision))
|
||||||
}
|
}
|
||||||
|
("GET", "/v1/users/quota") => {
|
||||||
|
let revision = current_revision(&shared.config_path).await?;
|
||||||
|
let disk_cfg = load_config_from_disk(&shared.config_path).await?;
|
||||||
|
let data = build_user_quota_list(&disk_cfg, shared.stats.as_ref());
|
||||||
|
Ok(success_response(StatusCode::OK, data, revision))
|
||||||
|
}
|
||||||
("POST", "/v1/users") => {
|
("POST", "/v1/users") => {
|
||||||
if api_cfg.read_only {
|
if api_cfg.read_only {
|
||||||
return Ok(error_response(
|
return Ok(error_response(
|
||||||
|
|||||||
@@ -473,6 +473,8 @@ pub(super) struct UserInfo {
|
|||||||
pub(super) max_tcp_conns: Option<usize>,
|
pub(super) max_tcp_conns: Option<usize>,
|
||||||
pub(super) expiration_rfc3339: Option<String>,
|
pub(super) expiration_rfc3339: Option<String>,
|
||||||
pub(super) data_quota_bytes: Option<u64>,
|
pub(super) data_quota_bytes: Option<u64>,
|
||||||
|
pub(super) rate_limit_up_bps: Option<u64>,
|
||||||
|
pub(super) rate_limit_down_bps: Option<u64>,
|
||||||
pub(super) max_unique_ips: Option<usize>,
|
pub(super) max_unique_ips: Option<usize>,
|
||||||
pub(super) current_connections: u64,
|
pub(super) current_connections: u64,
|
||||||
pub(super) active_unique_ips: usize,
|
pub(super) active_unique_ips: usize,
|
||||||
@@ -508,6 +510,19 @@ pub(super) struct ResetUserQuotaResponse {
|
|||||||
pub(super) last_reset_epoch_secs: u64,
|
pub(super) last_reset_epoch_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(super) struct UserQuotaListData {
|
||||||
|
pub(super) users: Vec<UserQuotaEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(super) struct UserQuotaEntry {
|
||||||
|
pub(super) username: String,
|
||||||
|
pub(super) data_quota_bytes: u64,
|
||||||
|
pub(super) used_bytes: u64,
|
||||||
|
pub(super) last_reset_epoch_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(super) struct CreateUserRequest {
|
pub(super) struct CreateUserRequest {
|
||||||
pub(super) username: String,
|
pub(super) username: String,
|
||||||
@@ -516,6 +531,8 @@ pub(super) struct CreateUserRequest {
|
|||||||
pub(super) max_tcp_conns: Option<usize>,
|
pub(super) max_tcp_conns: Option<usize>,
|
||||||
pub(super) expiration_rfc3339: Option<String>,
|
pub(super) expiration_rfc3339: Option<String>,
|
||||||
pub(super) data_quota_bytes: Option<u64>,
|
pub(super) data_quota_bytes: Option<u64>,
|
||||||
|
pub(super) rate_limit_up_bps: Option<u64>,
|
||||||
|
pub(super) rate_limit_down_bps: Option<u64>,
|
||||||
pub(super) max_unique_ips: Option<usize>,
|
pub(super) max_unique_ips: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,6 +548,10 @@ pub(super) struct PatchUserRequest {
|
|||||||
#[serde(default, deserialize_with = "patch_field")]
|
#[serde(default, deserialize_with = "patch_field")]
|
||||||
pub(super) data_quota_bytes: Patch<u64>,
|
pub(super) data_quota_bytes: Patch<u64>,
|
||||||
#[serde(default, deserialize_with = "patch_field")]
|
#[serde(default, deserialize_with = "patch_field")]
|
||||||
|
pub(super) rate_limit_up_bps: Patch<u64>,
|
||||||
|
#[serde(default, deserialize_with = "patch_field")]
|
||||||
|
pub(super) rate_limit_down_bps: Patch<u64>,
|
||||||
|
#[serde(default, deserialize_with = "patch_field")]
|
||||||
pub(super) max_unique_ips: Patch<usize>,
|
pub(super) max_unique_ips: Patch<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ mod tests {
|
|||||||
"secret": "00112233445566778899aabbccddeeff",
|
"secret": "00112233445566778899aabbccddeeff",
|
||||||
"max_tcp_conns": 0,
|
"max_tcp_conns": 0,
|
||||||
"max_unique_ips": null,
|
"max_unique_ips": null,
|
||||||
"data_quota_bytes": 1024
|
"data_quota_bytes": 1024,
|
||||||
|
"rate_limit_up_bps": 4096,
|
||||||
|
"rate_limit_down_bps": null
|
||||||
}"#;
|
}"#;
|
||||||
let req: PatchUserRequest = serde_json::from_str(raw).expect("valid json");
|
let req: PatchUserRequest = serde_json::from_str(raw).expect("valid json");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -124,6 +126,8 @@ mod tests {
|
|||||||
assert!(matches!(req.max_tcp_conns, Patch::Set(0)));
|
assert!(matches!(req.max_tcp_conns, Patch::Set(0)));
|
||||||
assert!(matches!(req.max_unique_ips, Patch::Remove));
|
assert!(matches!(req.max_unique_ips, Patch::Remove));
|
||||||
assert!(matches!(req.data_quota_bytes, Patch::Set(1024)));
|
assert!(matches!(req.data_quota_bytes, Patch::Set(1024)));
|
||||||
|
assert!(matches!(req.rate_limit_up_bps, Patch::Set(4096)));
|
||||||
|
assert!(matches!(req.rate_limit_down_bps, Patch::Remove));
|
||||||
assert!(matches!(req.expiration_rfc3339, Patch::Unchanged));
|
assert!(matches!(req.expiration_rfc3339, Patch::Unchanged));
|
||||||
assert!(matches!(req.user_ad_tag, Patch::Unchanged));
|
assert!(matches!(req.user_ad_tag, Patch::Unchanged));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ pub(super) async fn build_runtime_gates_data(
|
|||||||
cfg: &ProxyConfig,
|
cfg: &ProxyConfig,
|
||||||
) -> RuntimeGatesData {
|
) -> RuntimeGatesData {
|
||||||
let startup_summary = build_runtime_startup_summary(shared).await;
|
let startup_summary = build_runtime_startup_summary(shared).await;
|
||||||
|
let startup_snapshot = shared.startup_tracker.snapshot().await;
|
||||||
let route_state = shared.route_runtime.snapshot();
|
let route_state = shared.route_runtime.snapshot();
|
||||||
let route_mode = route_state.mode.as_str();
|
let route_mode = route_state.mode.as_str();
|
||||||
let fast_fallback_enabled =
|
let fast_fallback_enabled =
|
||||||
@@ -191,7 +192,9 @@ pub(super) async fn build_runtime_gates_data(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let reroute_reason = if reroute_active {
|
let reroute_reason = if reroute_active {
|
||||||
if fast_fallback_enabled {
|
if startup_snapshot.me.status.as_str() != "ready" {
|
||||||
|
Some("startup_direct_fallback")
|
||||||
|
} else if fast_fallback_enabled {
|
||||||
Some("fast_not_ready_fallback")
|
Some("fast_not_ready_fallback")
|
||||||
} else {
|
} else {
|
||||||
Some("strict_grace_fallback")
|
Some("strict_grace_fallback")
|
||||||
|
|||||||
186
src/api/users.rs
186
src/api/users.rs
@@ -3,6 +3,7 @@ use std::net::IpAddr;
|
|||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
|
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
|
use crate::config::RateLimitBps;
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
|
|
||||||
@@ -13,8 +14,9 @@ use super::config_store::{
|
|||||||
};
|
};
|
||||||
use super::model::{
|
use super::model::{
|
||||||
ApiFailure, CreateUserRequest, CreateUserResponse, PatchUserRequest, RotateSecretRequest,
|
ApiFailure, CreateUserRequest, CreateUserResponse, PatchUserRequest, RotateSecretRequest,
|
||||||
TlsDomainLink, UserInfo, UserLinks, is_valid_ad_tag, is_valid_user_secret, is_valid_username,
|
TlsDomainLink, UserInfo, UserLinks, UserQuotaEntry, UserQuotaListData, is_valid_ad_tag,
|
||||||
parse_optional_expiration, parse_patch_expiration, random_user_secret,
|
is_valid_user_secret, is_valid_username, parse_optional_expiration, parse_patch_expiration,
|
||||||
|
random_user_secret,
|
||||||
};
|
};
|
||||||
use super::patch::Patch;
|
use super::patch::Patch;
|
||||||
|
|
||||||
@@ -27,6 +29,8 @@ pub(super) async fn create_user(
|
|||||||
let touches_user_max_tcp_conns = body.max_tcp_conns.is_some();
|
let touches_user_max_tcp_conns = body.max_tcp_conns.is_some();
|
||||||
let touches_user_expirations = body.expiration_rfc3339.is_some();
|
let touches_user_expirations = body.expiration_rfc3339.is_some();
|
||||||
let touches_user_data_quota = body.data_quota_bytes.is_some();
|
let touches_user_data_quota = body.data_quota_bytes.is_some();
|
||||||
|
let touches_user_rate_limits =
|
||||||
|
body.rate_limit_up_bps.is_some() || body.rate_limit_down_bps.is_some();
|
||||||
let touches_user_max_unique_ips = body.max_unique_ips.is_some();
|
let touches_user_max_unique_ips = body.max_unique_ips.is_some();
|
||||||
|
|
||||||
if !is_valid_username(&body.username) {
|
if !is_valid_username(&body.username) {
|
||||||
@@ -91,6 +95,15 @@ pub(super) async fn create_user(
|
|||||||
.user_data_quota
|
.user_data_quota
|
||||||
.insert(body.username.clone(), quota);
|
.insert(body.username.clone(), quota);
|
||||||
}
|
}
|
||||||
|
if touches_user_rate_limits {
|
||||||
|
cfg.access.user_rate_limits.insert(
|
||||||
|
body.username.clone(),
|
||||||
|
RateLimitBps {
|
||||||
|
up_bps: body.rate_limit_up_bps.unwrap_or(0),
|
||||||
|
down_bps: body.rate_limit_down_bps.unwrap_or(0),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let updated_limit = body.max_unique_ips;
|
let updated_limit = body.max_unique_ips;
|
||||||
if let Some(limit) = updated_limit {
|
if let Some(limit) = updated_limit {
|
||||||
@@ -115,6 +128,9 @@ pub(super) async fn create_user(
|
|||||||
if touches_user_data_quota {
|
if touches_user_data_quota {
|
||||||
touched_sections.push(AccessSection::UserDataQuota);
|
touched_sections.push(AccessSection::UserDataQuota);
|
||||||
}
|
}
|
||||||
|
if touches_user_rate_limits {
|
||||||
|
touched_sections.push(AccessSection::UserRateLimits);
|
||||||
|
}
|
||||||
if touches_user_max_unique_ips {
|
if touches_user_max_unique_ips {
|
||||||
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
||||||
}
|
}
|
||||||
@@ -157,6 +173,8 @@ pub(super) async fn create_user(
|
|||||||
.then_some(cfg.access.user_max_tcp_conns_global_each)),
|
.then_some(cfg.access.user_max_tcp_conns_global_each)),
|
||||||
expiration_rfc3339: None,
|
expiration_rfc3339: None,
|
||||||
data_quota_bytes: None,
|
data_quota_bytes: None,
|
||||||
|
rate_limit_up_bps: body.rate_limit_up_bps.filter(|limit| *limit > 0),
|
||||||
|
rate_limit_down_bps: body.rate_limit_down_bps.filter(|limit| *limit > 0),
|
||||||
max_unique_ips: updated_limit,
|
max_unique_ips: updated_limit,
|
||||||
current_connections: 0,
|
current_connections: 0,
|
||||||
active_unique_ips: 0,
|
active_unique_ips: 0,
|
||||||
@@ -181,6 +199,8 @@ pub(super) async fn patch_user(
|
|||||||
let touches_user_max_tcp_conns = !matches!(&body.max_tcp_conns, Patch::Unchanged);
|
let touches_user_max_tcp_conns = !matches!(&body.max_tcp_conns, Patch::Unchanged);
|
||||||
let touches_user_expirations = !matches!(&body.expiration_rfc3339, Patch::Unchanged);
|
let touches_user_expirations = !matches!(&body.expiration_rfc3339, Patch::Unchanged);
|
||||||
let touches_user_data_quota = !matches!(&body.data_quota_bytes, Patch::Unchanged);
|
let touches_user_data_quota = !matches!(&body.data_quota_bytes, Patch::Unchanged);
|
||||||
|
let touches_user_rate_limits = !matches!(&body.rate_limit_up_bps, Patch::Unchanged)
|
||||||
|
|| !matches!(&body.rate_limit_down_bps, Patch::Unchanged);
|
||||||
let touches_user_max_unique_ips = !matches!(&body.max_unique_ips, Patch::Unchanged);
|
let touches_user_max_unique_ips = !matches!(&body.max_unique_ips, Patch::Unchanged);
|
||||||
|
|
||||||
if let Some(secret) = body.secret.as_ref()
|
if let Some(secret) = body.secret.as_ref()
|
||||||
@@ -253,6 +273,31 @@ pub(super) async fn patch_user(
|
|||||||
cfg.access.user_data_quota.insert(user.to_string(), quota);
|
cfg.access.user_data_quota.insert(user.to_string(), quota);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if touches_user_rate_limits {
|
||||||
|
let mut rate_limit = cfg
|
||||||
|
.access
|
||||||
|
.user_rate_limits
|
||||||
|
.get(user)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default();
|
||||||
|
match body.rate_limit_up_bps {
|
||||||
|
Patch::Unchanged => {}
|
||||||
|
Patch::Remove => rate_limit.up_bps = 0,
|
||||||
|
Patch::Set(limit) => rate_limit.up_bps = limit,
|
||||||
|
}
|
||||||
|
match body.rate_limit_down_bps {
|
||||||
|
Patch::Unchanged => {}
|
||||||
|
Patch::Remove => rate_limit.down_bps = 0,
|
||||||
|
Patch::Set(limit) => rate_limit.down_bps = limit,
|
||||||
|
}
|
||||||
|
if rate_limit.up_bps == 0 && rate_limit.down_bps == 0 {
|
||||||
|
cfg.access.user_rate_limits.remove(user);
|
||||||
|
} else {
|
||||||
|
cfg.access
|
||||||
|
.user_rate_limits
|
||||||
|
.insert(user.to_string(), rate_limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Capture how the per-user IP limit changed, so the in-memory ip_tracker
|
// Capture how the per-user IP limit changed, so the in-memory ip_tracker
|
||||||
// can be synced (set or removed) after the config is persisted.
|
// can be synced (set or removed) after the config is persisted.
|
||||||
let max_unique_ips_change = match body.max_unique_ips {
|
let max_unique_ips_change = match body.max_unique_ips {
|
||||||
@@ -288,6 +333,9 @@ pub(super) async fn patch_user(
|
|||||||
if touches_user_data_quota {
|
if touches_user_data_quota {
|
||||||
touched_sections.push(AccessSection::UserDataQuota);
|
touched_sections.push(AccessSection::UserDataQuota);
|
||||||
}
|
}
|
||||||
|
if touches_user_rate_limits {
|
||||||
|
touched_sections.push(AccessSection::UserRateLimits);
|
||||||
|
}
|
||||||
if touches_user_max_unique_ips {
|
if touches_user_max_unique_ips {
|
||||||
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
||||||
}
|
}
|
||||||
@@ -355,6 +403,7 @@ pub(super) async fn rotate_secret(
|
|||||||
AccessSection::UserMaxTcpConns,
|
AccessSection::UserMaxTcpConns,
|
||||||
AccessSection::UserExpirations,
|
AccessSection::UserExpirations,
|
||||||
AccessSection::UserDataQuota,
|
AccessSection::UserDataQuota,
|
||||||
|
AccessSection::UserRateLimits,
|
||||||
AccessSection::UserMaxUniqueIps,
|
AccessSection::UserMaxUniqueIps,
|
||||||
];
|
];
|
||||||
let revision =
|
let revision =
|
||||||
@@ -414,6 +463,7 @@ pub(super) async fn delete_user(
|
|||||||
cfg.access.user_max_tcp_conns.remove(user);
|
cfg.access.user_max_tcp_conns.remove(user);
|
||||||
cfg.access.user_expirations.remove(user);
|
cfg.access.user_expirations.remove(user);
|
||||||
cfg.access.user_data_quota.remove(user);
|
cfg.access.user_data_quota.remove(user);
|
||||||
|
cfg.access.user_rate_limits.remove(user);
|
||||||
cfg.access.user_max_unique_ips.remove(user);
|
cfg.access.user_max_unique_ips.remove(user);
|
||||||
|
|
||||||
cfg.validate()
|
cfg.validate()
|
||||||
@@ -424,6 +474,7 @@ pub(super) async fn delete_user(
|
|||||||
AccessSection::UserMaxTcpConns,
|
AccessSection::UserMaxTcpConns,
|
||||||
AccessSection::UserExpirations,
|
AccessSection::UserExpirations,
|
||||||
AccessSection::UserDataQuota,
|
AccessSection::UserDataQuota,
|
||||||
|
AccessSection::UserRateLimits,
|
||||||
AccessSection::UserMaxUniqueIps,
|
AccessSection::UserMaxUniqueIps,
|
||||||
];
|
];
|
||||||
let revision =
|
let revision =
|
||||||
@@ -485,6 +536,18 @@ pub(super) async fn users_from_config(
|
|||||||
.get(&username)
|
.get(&username)
|
||||||
.map(chrono::DateTime::<chrono::Utc>::to_rfc3339),
|
.map(chrono::DateTime::<chrono::Utc>::to_rfc3339),
|
||||||
data_quota_bytes: cfg.access.user_data_quota.get(&username).copied(),
|
data_quota_bytes: cfg.access.user_data_quota.get(&username).copied(),
|
||||||
|
rate_limit_up_bps: cfg
|
||||||
|
.access
|
||||||
|
.user_rate_limits
|
||||||
|
.get(&username)
|
||||||
|
.map(|limit| limit.up_bps)
|
||||||
|
.filter(|limit| *limit > 0),
|
||||||
|
rate_limit_down_bps: cfg
|
||||||
|
.access
|
||||||
|
.user_rate_limits
|
||||||
|
.get(&username)
|
||||||
|
.map(|limit| limit.down_bps)
|
||||||
|
.filter(|limit| *limit > 0),
|
||||||
max_unique_ips: cfg
|
max_unique_ips: cfg
|
||||||
.access
|
.access
|
||||||
.user_max_unique_ips
|
.user_max_unique_ips
|
||||||
@@ -506,6 +569,33 @@ pub(super) async fn users_from_config(
|
|||||||
users
|
users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn build_user_quota_list(cfg: &ProxyConfig, stats: &Stats) -> UserQuotaListData {
|
||||||
|
let mut names = cfg.access.users.keys().cloned().collect::<Vec<_>>();
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
let snapshot = stats.user_quota_snapshot();
|
||||||
|
let mut users = Vec::with_capacity(names.len());
|
||||||
|
for username in names {
|
||||||
|
let Some(&data_quota_bytes) = cfg.access.user_data_quota.get(&username) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if data_quota_bytes == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (used_bytes, last_reset_epoch_secs) = snapshot
|
||||||
|
.get(&username)
|
||||||
|
.map(|entry| (entry.used_bytes, entry.last_reset_epoch_secs))
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
users.push(UserQuotaEntry {
|
||||||
|
username,
|
||||||
|
data_quota_bytes,
|
||||||
|
used_bytes,
|
||||||
|
last_reset_epoch_secs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
UserQuotaListData { users }
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_user_links() -> UserLinks {
|
fn empty_user_links() -> UserLinks {
|
||||||
UserLinks {
|
UserLinks {
|
||||||
classic: Vec::new(),
|
classic: Vec::new(),
|
||||||
@@ -758,6 +848,34 @@ mod tests {
|
|||||||
assert_eq!(alice.max_tcp_conns, None);
|
assert_eq!(alice.max_tcp_conns, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn users_from_config_reports_user_rate_limits() {
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.access.users.insert(
|
||||||
|
"alice".to_string(),
|
||||||
|
"0123456789abcdef0123456789abcdef".to_string(),
|
||||||
|
);
|
||||||
|
cfg.access.user_rate_limits.insert(
|
||||||
|
"alice".to_string(),
|
||||||
|
RateLimitBps {
|
||||||
|
up_bps: 1024,
|
||||||
|
down_bps: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let stats = Stats::new();
|
||||||
|
let tracker = UserIpTracker::new();
|
||||||
|
|
||||||
|
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
||||||
|
let alice = users
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.username == "alice")
|
||||||
|
.expect("alice must be present");
|
||||||
|
|
||||||
|
assert_eq!(alice.rate_limit_up_bps, Some(1024));
|
||||||
|
assert_eq!(alice.rate_limit_down_bps, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn users_from_config_marks_runtime_membership_when_snapshot_is_provided() {
|
async fn users_from_config_marks_runtime_membership_when_snapshot_is_provided() {
|
||||||
let mut disk_cfg = ProxyConfig::default();
|
let mut disk_cfg = ProxyConfig::default();
|
||||||
@@ -869,4 +987,68 @@ mod tests {
|
|||||||
.any(|entry| entry.domain == "front-a.example.com")
|
.any(|entry| entry.domain == "front-a.example.com")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_user_quota_list_skips_users_without_positive_quota_and_sorts_by_username() {
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.access.users.insert(
|
||||||
|
"alice".to_string(),
|
||||||
|
"0123456789abcdef0123456789abcdef".to_string(),
|
||||||
|
);
|
||||||
|
cfg.access.users.insert(
|
||||||
|
"bob".to_string(),
|
||||||
|
"fedcba9876543210fedcba9876543210".to_string(),
|
||||||
|
);
|
||||||
|
cfg.access.users.insert(
|
||||||
|
"carol".to_string(),
|
||||||
|
"aaaabbbbccccddddeeeeffff00001111".to_string(),
|
||||||
|
);
|
||||||
|
// alice has a positive quota and should be listed.
|
||||||
|
cfg.access
|
||||||
|
.user_data_quota
|
||||||
|
.insert("alice".to_string(), 1 << 20);
|
||||||
|
// bob has no quota entry at all (None) — should be skipped.
|
||||||
|
// carol has an explicit zero quota — should be skipped.
|
||||||
|
cfg.access.user_data_quota.insert("carol".to_string(), 0);
|
||||||
|
|
||||||
|
let stats = Stats::new();
|
||||||
|
// Charge some traffic against alice; carol gets traffic too but should
|
||||||
|
// still be filtered out by the quota check.
|
||||||
|
let alice_stats = stats.get_or_create_user_stats_handle("alice");
|
||||||
|
stats.quota_charge_post_write(&alice_stats, 4096);
|
||||||
|
let carol_stats = stats.get_or_create_user_stats_handle("carol");
|
||||||
|
stats.quota_charge_post_write(&carol_stats, 99);
|
||||||
|
|
||||||
|
let data = build_user_quota_list(&cfg, &stats);
|
||||||
|
|
||||||
|
assert_eq!(data.users.len(), 1);
|
||||||
|
let entry = &data.users[0];
|
||||||
|
assert_eq!(entry.username, "alice");
|
||||||
|
assert_eq!(entry.data_quota_bytes, 1 << 20);
|
||||||
|
assert_eq!(entry.used_bytes, 4096);
|
||||||
|
assert_eq!(entry.last_reset_epoch_secs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_user_quota_list_orders_multiple_users_by_username_ascending() {
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
for name in ["charlie", "alice", "bob"] {
|
||||||
|
cfg.access.users.insert(
|
||||||
|
name.to_string(),
|
||||||
|
"0123456789abcdef0123456789abcdef".to_string(),
|
||||||
|
);
|
||||||
|
cfg.access.user_data_quota.insert(name.to_string(), 1 << 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats = Stats::new();
|
||||||
|
let data = build_user_quota_list(&cfg, &stats);
|
||||||
|
|
||||||
|
let names: Vec<&str> = data.users.iter().map(|e| e.username.as_str()).collect();
|
||||||
|
assert_eq!(names, vec!["alice", "bob", "charlie"]);
|
||||||
|
for entry in &data.users {
|
||||||
|
assert_eq!(entry.used_bytes, 0);
|
||||||
|
assert_eq!(entry.last_reset_epoch_secs, 0);
|
||||||
|
assert_eq!(entry.data_quota_bytes, 1 << 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -617,6 +617,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
|| old.censorship.mask != new.censorship.mask
|
|| old.censorship.mask != new.censorship.mask
|
||||||
|| old.censorship.mask_host != new.censorship.mask_host
|
|| old.censorship.mask_host != new.censorship.mask_host
|
||||||
|| old.censorship.mask_port != new.censorship.mask_port
|
|| old.censorship.mask_port != new.censorship.mask_port
|
||||||
|
|| old.censorship.exclusive_mask != new.censorship.exclusive_mask
|
||||||
|| old.censorship.mask_unix_sock != new.censorship.mask_unix_sock
|
|| old.censorship.mask_unix_sock != new.censorship.mask_unix_sock
|
||||||
|| old.censorship.fake_cert_len != new.censorship.fake_cert_len
|
|| old.censorship.fake_cert_len != new.censorship.fake_cert_len
|
||||||
|| old.censorship.tls_emulation != new.censorship.tls_emulation
|
|| old.censorship.tls_emulation != new.censorship.tls_emulation
|
||||||
|
|||||||
@@ -31,6 +31,84 @@ fn is_valid_tls_domain_name(domain: &str) -> bool {
|
|||||||
.any(|ch| ch.is_whitespace() || matches!(ch, '/' | '\\'))
|
.any(|ch| ch.is_whitespace() || matches!(ch, '/' | '\\'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_domain_to_ascii(domain: &str, field: &str) -> Result<String> {
|
||||||
|
let domain = domain.trim();
|
||||||
|
if !is_valid_tls_domain_name(domain) {
|
||||||
|
return Err(ProxyError::Config(format!(
|
||||||
|
"Invalid {field}: '{}'. Must be a valid domain name",
|
||||||
|
domain
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = url::Url::parse(&format!("https://{domain}/")).map_err(|error| {
|
||||||
|
ProxyError::Config(format!(
|
||||||
|
"Invalid {field}: '{}'. IDNA conversion failed: {error}",
|
||||||
|
domain
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let host = parsed.host_str().ok_or_else(|| {
|
||||||
|
ProxyError::Config(format!("Invalid {field}: '{}'. Host is empty", domain))
|
||||||
|
})?;
|
||||||
|
Ok(host.to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_mask_host_to_ascii(host: &str, field: &str) -> Result<String> {
|
||||||
|
let host = host.trim();
|
||||||
|
if host.starts_with('[') && host.ends_with(']') {
|
||||||
|
let inner = &host[1..host.len() - 1];
|
||||||
|
let ip = inner.parse::<std::net::IpAddr>().map_err(|_| {
|
||||||
|
ProxyError::Config(format!("Invalid {field}: '{}'. IPv6 literal is invalid", host))
|
||||||
|
})?;
|
||||||
|
return match ip {
|
||||||
|
std::net::IpAddr::V6(v6) => Ok(format!("[{v6}]")),
|
||||||
|
std::net::IpAddr::V4(v4) => Ok(v4.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||||
|
return match ip {
|
||||||
|
std::net::IpAddr::V4(v4) => Ok(v4.to_string()),
|
||||||
|
std::net::IpAddr::V6(v6) => Ok(format!("[{v6}]")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_domain_to_ascii(host, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_exclusive_mask_target(target: &str) -> Option<(&str, u16)> {
|
||||||
|
let target = target.trim();
|
||||||
|
if target.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.starts_with('[') {
|
||||||
|
let end = target.find(']')?;
|
||||||
|
if target.get(end + 1..end + 2)? != ":" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let host = &target[..=end];
|
||||||
|
let port = target[end + 2..].parse::<u16>().ok()?;
|
||||||
|
return (port > 0).then_some((host, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (host, port) = target.rsplit_once(':')?;
|
||||||
|
if host.is_empty() || host.contains(':') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let port = port.parse::<u16>().ok()?;
|
||||||
|
(port > 0).then_some((host, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_exclusive_mask_target(target: &str, field: &str) -> Result<String> {
|
||||||
|
let (host, port) = parse_exclusive_mask_target(target).ok_or_else(|| {
|
||||||
|
ProxyError::Config(format!(
|
||||||
|
"Invalid {field}: '{}'. Expected host:port with port > 0",
|
||||||
|
target
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let host = normalize_mask_host_to_ascii(host, field)?;
|
||||||
|
Ok(format!("{host}:{port}"))
|
||||||
|
}
|
||||||
|
|
||||||
const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
|
const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
|
||||||
"general",
|
"general",
|
||||||
"network",
|
"network",
|
||||||
@@ -291,6 +369,7 @@ const CENSORSHIP_CONFIG_KEYS: &[&str] = &[
|
|||||||
"mask",
|
"mask",
|
||||||
"mask_host",
|
"mask_host",
|
||||||
"mask_port",
|
"mask_port",
|
||||||
|
"exclusive_mask",
|
||||||
"mask_unix_sock",
|
"mask_unix_sock",
|
||||||
"fake_cert_len",
|
"fake_cert_len",
|
||||||
"tls_emulation",
|
"tls_emulation",
|
||||||
@@ -1887,10 +1966,8 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate tls_domain.
|
config.censorship.tls_domain =
|
||||||
if config.censorship.tls_domain.is_empty() {
|
normalize_domain_to_ascii(&config.censorship.tls_domain, "censorship.tls_domain")?;
|
||||||
return Err(ProxyError::Config("tls_domain cannot be empty".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate mask_unix_sock.
|
// Validate mask_unix_sock.
|
||||||
if let Some(ref sock_path) = config.censorship.mask_unix_sock {
|
if let Some(ref sock_path) = config.censorship.mask_unix_sock {
|
||||||
@@ -1918,11 +1995,30 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(mask_host) = config.censorship.mask_host.as_mut() {
|
||||||
|
*mask_host = normalize_mask_host_to_ascii(mask_host, "censorship.mask_host")?;
|
||||||
|
}
|
||||||
|
|
||||||
// Default mask_host to tls_domain if not set and no unix socket configured.
|
// Default mask_host to tls_domain if not set and no unix socket configured.
|
||||||
if config.censorship.mask_host.is_none() && config.censorship.mask_unix_sock.is_none() {
|
if config.censorship.mask_host.is_none() && config.censorship.mask_unix_sock.is_none() {
|
||||||
config.censorship.mask_host = Some(config.censorship.tls_domain.clone());
|
config.censorship.mask_host = Some(config.censorship.tls_domain.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (domain, target) in &config.censorship.exclusive_mask {
|
||||||
|
if !is_valid_tls_domain_name(domain) {
|
||||||
|
return Err(ProxyError::Config(format!(
|
||||||
|
"Invalid censorship.exclusive_mask domain: '{}'. Must be a valid domain name",
|
||||||
|
domain
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if parse_exclusive_mask_target(target).is_none() {
|
||||||
|
return Err(ProxyError::Config(format!(
|
||||||
|
"Invalid censorship.exclusive_mask target for '{}': '{}'. Expected host:port with port > 0",
|
||||||
|
domain, target
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize optional TLS fetch scope: whitespace-only values disable scoped routing.
|
// Normalize optional TLS fetch scope: whitespace-only values disable scoped routing.
|
||||||
config.censorship.tls_fetch_scope = config.censorship.tls_fetch_scope.trim().to_string();
|
config.censorship.tls_fetch_scope = config.censorship.tls_fetch_scope.trim().to_string();
|
||||||
|
|
||||||
@@ -1953,8 +2049,11 @@ impl ProxyConfig {
|
|||||||
let mut all = Vec::with_capacity(1 + config.censorship.tls_domains.len());
|
let mut all = Vec::with_capacity(1 + config.censorship.tls_domains.len());
|
||||||
all.push(config.censorship.tls_domain.clone());
|
all.push(config.censorship.tls_domain.clone());
|
||||||
for d in std::mem::take(&mut config.censorship.tls_domains) {
|
for d in std::mem::take(&mut config.censorship.tls_domains) {
|
||||||
if !d.is_empty() && !all.contains(&d) {
|
if !d.is_empty() {
|
||||||
all.push(d);
|
let domain = normalize_domain_to_ascii(&d, "censorship.tls_domains entry")?;
|
||||||
|
if !all.contains(&domain) {
|
||||||
|
all.push(domain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// keep primary as tls_domain; store remaining back to tls_domains
|
// keep primary as tls_domain; store remaining back to tls_domains
|
||||||
@@ -1963,6 +2062,20 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut exclusive_mask = HashMap::with_capacity(config.censorship.exclusive_mask.len());
|
||||||
|
for (domain, target) in std::mem::take(&mut config.censorship.exclusive_mask) {
|
||||||
|
let domain = normalize_domain_to_ascii(
|
||||||
|
&domain,
|
||||||
|
"censorship.exclusive_mask domain",
|
||||||
|
)?;
|
||||||
|
let target = normalize_exclusive_mask_target(
|
||||||
|
&target,
|
||||||
|
"censorship.exclusive_mask target",
|
||||||
|
)?;
|
||||||
|
exclusive_mask.insert(domain, target);
|
||||||
|
}
|
||||||
|
config.censorship.exclusive_mask = exclusive_mask;
|
||||||
|
|
||||||
// Migration: prefer_ipv6 -> network.prefer.
|
// Migration: prefer_ipv6 -> network.prefer.
|
||||||
if config.general.prefer_ipv6 {
|
if config.general.prefer_ipv6 {
|
||||||
if config.network.prefer == 4 {
|
if config.network.prefer == 4 {
|
||||||
@@ -2126,6 +2239,21 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (domain, target) in &self.censorship.exclusive_mask {
|
||||||
|
if !is_valid_tls_domain_name(domain) {
|
||||||
|
return Err(ProxyError::Config(format!(
|
||||||
|
"Invalid censorship.exclusive_mask domain: '{}'. Must be a valid domain name",
|
||||||
|
domain
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if parse_exclusive_mask_target(target).is_none() {
|
||||||
|
return Err(ProxyError::Config(format!(
|
||||||
|
"Invalid censorship.exclusive_mask target for '{}': '{}'. Expected host:port with port > 0",
|
||||||
|
domain, target
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (user, tag) in &self.access.user_ad_tags {
|
for (user, tag) in &self.access.user_ad_tags {
|
||||||
let zeros = "00000000000000000000000000000000";
|
let zeros = "00000000000000000000000000000000";
|
||||||
if !is_valid_ad_tag(tag) {
|
if !is_valid_ad_tag(tag) {
|
||||||
@@ -2667,6 +2795,44 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exclusive_mask_parses_domain_target_map() {
|
||||||
|
let cfg = load_config_from_temp_toml(
|
||||||
|
r#"
|
||||||
|
[general]
|
||||||
|
[network]
|
||||||
|
[server]
|
||||||
|
[access]
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "weißbiergärten.de"
|
||||||
|
tls_domains = ["bürgeramt.de"]
|
||||||
|
[censorship.exclusive_mask]
|
||||||
|
"bürgeramt.de" = "rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz.de:443"
|
||||||
|
"ipv6.example" = "[::1]:443"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(cfg.censorship.tls_domain.is_ascii());
|
||||||
|
assert!(cfg.censorship.tls_domain.contains("xn--"));
|
||||||
|
assert_eq!(cfg.censorship.tls_domains.len(), 1);
|
||||||
|
let normalized_extra = &cfg.censorship.tls_domains[0];
|
||||||
|
assert!(normalized_extra.is_ascii());
|
||||||
|
assert!(normalized_extra.contains("xn--"));
|
||||||
|
|
||||||
|
let normalized_target = cfg
|
||||||
|
.censorship
|
||||||
|
.exclusive_mask
|
||||||
|
.get(normalized_extra)
|
||||||
|
.expect("exclusive_mask key must match normalized tls_domains entry");
|
||||||
|
assert!(normalized_target.is_ascii());
|
||||||
|
assert!(normalized_target.contains("xn--"));
|
||||||
|
assert!(normalized_target.ends_with(":443"));
|
||||||
|
assert_eq!(
|
||||||
|
cfg.censorship.exclusive_mask.get("ipv6.example"),
|
||||||
|
Some(&"[::1]:443".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn api_gray_action_parses_and_defaults_to_drop() {
|
fn api_gray_action_parses_and_defaults_to_drop() {
|
||||||
let cfg_default: ProxyConfig = toml::from_str(
|
let cfg_default: ProxyConfig = toml::from_str(
|
||||||
|
|||||||
@@ -1719,6 +1719,10 @@ pub struct AntiCensorshipConfig {
|
|||||||
#[serde(default = "default_mask_port")]
|
#[serde(default = "default_mask_port")]
|
||||||
pub mask_port: u16,
|
pub mask_port: u16,
|
||||||
|
|
||||||
|
/// Per-SNI TCP mask targets. Keys are SNI domains, values are `host:port`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub exclusive_mask: HashMap<String, String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mask_unix_sock: Option<String>,
|
pub mask_unix_sock: Option<String>,
|
||||||
|
|
||||||
@@ -1842,6 +1846,7 @@ impl Default for AntiCensorshipConfig {
|
|||||||
mask: default_true(),
|
mask: default_true(),
|
||||||
mask_host: None,
|
mask_host: None,
|
||||||
mask_port: default_mask_port(),
|
mask_port: default_mask_port(),
|
||||||
|
exclusive_mask: HashMap::new(),
|
||||||
mask_unix_sock: None,
|
mask_unix_sock: None,
|
||||||
fake_cert_len: default_fake_cert_len(),
|
fake_cert_len: default_fake_cert_len(),
|
||||||
tls_emulation: true,
|
tls_emulation: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use tokio::sync::watch;
|
use tokio::sync::{RwLock, watch};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
@@ -14,24 +14,32 @@ const RUNTIME_FALLBACK_AFTER: Duration = Duration::from_secs(6);
|
|||||||
pub(crate) async fn configure_admission_gate(
|
pub(crate) async fn configure_admission_gate(
|
||||||
config: &Arc<ProxyConfig>,
|
config: &Arc<ProxyConfig>,
|
||||||
me_pool: Option<Arc<MePool>>,
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
me_pool_runtime: Arc<RwLock<Option<Arc<MePool>>>>,
|
||||||
route_runtime: Arc<RouteRuntimeController>,
|
route_runtime: Arc<RouteRuntimeController>,
|
||||||
admission_tx: &watch::Sender<bool>,
|
admission_tx: &watch::Sender<bool>,
|
||||||
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||||
me_ready_rx: watch::Receiver<u64>,
|
me_ready_rx: watch::Receiver<u64>,
|
||||||
) {
|
) {
|
||||||
if config.general.use_middle_proxy {
|
if config.general.use_middle_proxy {
|
||||||
if let Some(pool) = me_pool.as_ref() {
|
if me_pool.is_some() || config.general.me2dc_fallback {
|
||||||
let initial_ready = pool.admission_ready_conditional_cast().await;
|
let initial_pool = match me_pool.as_ref() {
|
||||||
|
Some(pool) => Some(pool.clone()),
|
||||||
|
None => me_pool_runtime.read().await.clone(),
|
||||||
|
};
|
||||||
|
let initial_ready = match initial_pool.as_ref() {
|
||||||
|
Some(pool) => pool.admission_ready_conditional_cast().await,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
let mut fallback_enabled = config.general.me2dc_fallback;
|
let mut fallback_enabled = config.general.me2dc_fallback;
|
||||||
let mut fast_fallback_enabled = fallback_enabled && config.general.me2dc_fast;
|
let mut fast_fallback_enabled = fallback_enabled && config.general.me2dc_fast;
|
||||||
let (initial_gate_open, initial_route_mode, initial_fallback_reason) = if initial_ready
|
let (initial_gate_open, initial_route_mode, initial_fallback_reason) = if initial_ready
|
||||||
{
|
{
|
||||||
(true, RelayRouteMode::Middle, None)
|
(true, RelayRouteMode::Middle, None)
|
||||||
} else if fast_fallback_enabled {
|
} else if fallback_enabled {
|
||||||
(
|
(
|
||||||
true,
|
true,
|
||||||
RelayRouteMode::Direct,
|
RelayRouteMode::Direct,
|
||||||
Some("fast_not_ready_fallback"),
|
Some("startup_direct_fallback"),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(false, RelayRouteMode::Middle, None)
|
(false, RelayRouteMode::Middle, None)
|
||||||
@@ -49,7 +57,8 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
warn!("Conditional-admission gate: closed / ME pool is NOT ready)");
|
warn!("Conditional-admission gate: closed / ME pool is NOT ready)");
|
||||||
}
|
}
|
||||||
|
|
||||||
let pool_for_gate = pool.clone();
|
let mut pool_for_gate = initial_pool;
|
||||||
|
let pool_runtime_for_gate = me_pool_runtime.clone();
|
||||||
let admission_tx_gate = admission_tx.clone();
|
let admission_tx_gate = admission_tx.clone();
|
||||||
let route_runtime_gate = route_runtime.clone();
|
let route_runtime_gate = route_runtime.clone();
|
||||||
let mut config_rx_gate = config_rx.clone();
|
let mut config_rx_gate = config_rx.clone();
|
||||||
@@ -83,12 +92,27 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
}
|
}
|
||||||
_ = tokio::time::sleep(Duration::from_millis(admission_poll_ms)) => {}
|
_ = tokio::time::sleep(Duration::from_millis(admission_poll_ms)) => {}
|
||||||
}
|
}
|
||||||
let ready = pool_for_gate.admission_ready_conditional_cast().await;
|
if pool_for_gate.is_none() {
|
||||||
|
pool_for_gate = pool_runtime_for_gate.read().await.clone();
|
||||||
|
}
|
||||||
|
let ready = match pool_for_gate.as_ref() {
|
||||||
|
Some(pool) => pool.admission_ready_conditional_cast().await,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let (next_gate_open, next_route_mode, next_fallback_reason) = if ready {
|
let (next_gate_open, next_route_mode, next_fallback_reason) = if ready {
|
||||||
ready_observed = true;
|
ready_observed = true;
|
||||||
not_ready_since = None;
|
not_ready_since = None;
|
||||||
|
if let Some(pool) = pool_for_gate.as_ref() {
|
||||||
|
pool.set_runtime_ready(true);
|
||||||
|
}
|
||||||
(true, RelayRouteMode::Middle, None)
|
(true, RelayRouteMode::Middle, None)
|
||||||
|
} else if fallback_enabled && !ready_observed {
|
||||||
|
(
|
||||||
|
true,
|
||||||
|
RelayRouteMode::Direct,
|
||||||
|
Some("startup_direct_fallback"),
|
||||||
|
)
|
||||||
} else if fast_fallback_enabled {
|
} else if fast_fallback_enabled {
|
||||||
(
|
(
|
||||||
true,
|
true,
|
||||||
@@ -122,7 +146,14 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let fallback_reason = next_fallback_reason.unwrap_or("unknown");
|
let fallback_reason = next_fallback_reason.unwrap_or("unknown");
|
||||||
if fallback_reason == "strict_grace_fallback" {
|
if fallback_reason == "startup_direct_fallback" {
|
||||||
|
warn!(
|
||||||
|
target_mode = route_mode.as_str(),
|
||||||
|
cutover_generation = snapshot.generation,
|
||||||
|
fallback_reason,
|
||||||
|
"ME pool not-ready during startup; routing new sessions via Direct-DC"
|
||||||
|
);
|
||||||
|
} else if fallback_reason == "strict_grace_fallback" {
|
||||||
let fallback_after = if ready_observed {
|
let fallback_after = if ready_observed {
|
||||||
RUNTIME_FALLBACK_AFTER
|
RUNTIME_FALLBACK_AFTER
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::time::Duration;
|
|||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use tokio::net::UnixListener;
|
use tokio::net::UnixListener;
|
||||||
use tokio::sync::{Semaphore, watch};
|
use tokio::sync::{RwLock, Semaphore, watch};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::config::{ProxyConfig, RstOnCloseMode};
|
use crate::config::{ProxyConfig, RstOnCloseMode};
|
||||||
@@ -63,6 +63,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
buffer_pool: Arc<BufferPool>,
|
buffer_pool: Arc<BufferPool>,
|
||||||
rng: Arc<SecureRandom>,
|
rng: Arc<SecureRandom>,
|
||||||
me_pool: Option<Arc<MePool>>,
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
me_pool_runtime: Arc<RwLock<Option<Arc<MePool>>>>,
|
||||||
route_runtime: Arc<RouteRuntimeController>,
|
route_runtime: Arc<RouteRuntimeController>,
|
||||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
@@ -236,6 +237,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
let buffer_pool = buffer_pool.clone();
|
let buffer_pool = buffer_pool.clone();
|
||||||
let rng = rng.clone();
|
let rng = rng.clone();
|
||||||
let me_pool = me_pool.clone();
|
let me_pool = me_pool.clone();
|
||||||
|
let me_pool_runtime = me_pool_runtime.clone();
|
||||||
let route_runtime = route_runtime.clone();
|
let route_runtime = route_runtime.clone();
|
||||||
let tls_cache = tls_cache.clone();
|
let tls_cache = tls_cache.clone();
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
@@ -298,6 +300,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
let buffer_pool = buffer_pool.clone();
|
let buffer_pool = buffer_pool.clone();
|
||||||
let rng = rng.clone();
|
let rng = rng.clone();
|
||||||
let me_pool = me_pool.clone();
|
let me_pool = me_pool.clone();
|
||||||
|
let me_pool_runtime = me_pool_runtime.clone();
|
||||||
let route_runtime = route_runtime.clone();
|
let route_runtime = route_runtime.clone();
|
||||||
let tls_cache = tls_cache.clone();
|
let tls_cache = tls_cache.clone();
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
@@ -307,7 +310,8 @@ pub(crate) async fn bind_listeners(
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _permit = permit;
|
let _permit = permit;
|
||||||
if let Err(e) = crate::proxy::client::handle_client_stream_with_shared(
|
if let Err(e) =
|
||||||
|
crate::proxy::client::handle_client_stream_with_shared_and_pool_runtime(
|
||||||
stream,
|
stream,
|
||||||
fake_peer,
|
fake_peer,
|
||||||
config,
|
config,
|
||||||
@@ -317,6 +321,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
buffer_pool,
|
buffer_pool,
|
||||||
rng,
|
rng,
|
||||||
me_pool,
|
me_pool,
|
||||||
|
Some(me_pool_runtime),
|
||||||
route_runtime,
|
route_runtime,
|
||||||
tls_cache,
|
tls_cache,
|
||||||
ip_tracker,
|
ip_tracker,
|
||||||
@@ -367,6 +372,7 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
buffer_pool: Arc<BufferPool>,
|
buffer_pool: Arc<BufferPool>,
|
||||||
rng: Arc<SecureRandom>,
|
rng: Arc<SecureRandom>,
|
||||||
me_pool: Option<Arc<MePool>>,
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
me_pool_runtime: Arc<RwLock<Option<Arc<MePool>>>>,
|
||||||
route_runtime: Arc<RouteRuntimeController>,
|
route_runtime: Arc<RouteRuntimeController>,
|
||||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
@@ -383,6 +389,7 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
let buffer_pool = buffer_pool.clone();
|
let buffer_pool = buffer_pool.clone();
|
||||||
let rng = rng.clone();
|
let rng = rng.clone();
|
||||||
let me_pool = me_pool.clone();
|
let me_pool = me_pool.clone();
|
||||||
|
let me_pool_runtime = me_pool_runtime.clone();
|
||||||
let route_runtime = route_runtime.clone();
|
let route_runtime = route_runtime.clone();
|
||||||
let tls_cache = tls_cache.clone();
|
let tls_cache = tls_cache.clone();
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
@@ -449,6 +456,7 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
let buffer_pool = buffer_pool.clone();
|
let buffer_pool = buffer_pool.clone();
|
||||||
let rng = rng.clone();
|
let rng = rng.clone();
|
||||||
let me_pool = me_pool.clone();
|
let me_pool = me_pool.clone();
|
||||||
|
let me_pool_runtime = me_pool_runtime.clone();
|
||||||
let route_runtime = route_runtime.clone();
|
let route_runtime = route_runtime.clone();
|
||||||
let tls_cache = tls_cache.clone();
|
let tls_cache = tls_cache.clone();
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
@@ -470,6 +478,7 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
buffer_pool,
|
buffer_pool,
|
||||||
rng,
|
rng,
|
||||||
me_pool,
|
me_pool,
|
||||||
|
Some(me_pool_runtime),
|
||||||
route_runtime,
|
route_runtime,
|
||||||
tls_cache,
|
tls_cache,
|
||||||
ip_tracker,
|
ip_tracker,
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ use crate::network::probe::{decide_network_capabilities, log_probe_result, run_p
|
|||||||
use crate::proxy::route_mode::{RelayRouteMode, RouteRuntimeController};
|
use crate::proxy::route_mode::{RelayRouteMode, RouteRuntimeController};
|
||||||
use crate::proxy::shared_state::ProxySharedState;
|
use crate::proxy::shared_state::ProxySharedState;
|
||||||
use crate::startup::{
|
use crate::startup::{
|
||||||
COMPONENT_API_BOOTSTRAP, COMPONENT_CONFIG_LOAD, COMPONENT_ME_POOL_CONSTRUCT,
|
COMPONENT_API_BOOTSTRAP, COMPONENT_CONFIG_LOAD, COMPONENT_DC_CONNECTIVITY_PING,
|
||||||
COMPONENT_ME_POOL_INIT_STAGE1, COMPONENT_ME_PROXY_CONFIG_V4, COMPONENT_ME_PROXY_CONFIG_V6,
|
COMPONENT_ME_CONNECTIVITY_PING, COMPONENT_ME_POOL_CONSTRUCT, COMPONENT_ME_POOL_INIT_STAGE1,
|
||||||
COMPONENT_ME_SECRET_FETCH, COMPONENT_NETWORK_PROBE, COMPONENT_TRACING_INIT, StartupMeStatus,
|
COMPONENT_ME_PROXY_CONFIG_V4, COMPONENT_ME_PROXY_CONFIG_V6, COMPONENT_ME_SECRET_FETCH,
|
||||||
StartupTracker,
|
COMPONENT_NETWORK_PROBE, COMPONENT_TRACING_INIT, StartupMeStatus, StartupTracker,
|
||||||
};
|
};
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::stats::telemetry::TelemetryPolicy;
|
use crate::stats::telemetry::TelemetryPolicy;
|
||||||
@@ -461,12 +461,14 @@ async fn run_telemt_core(
|
|||||||
|
|
||||||
let (api_config_tx, api_config_rx) = watch::channel(Arc::new(config.clone()));
|
let (api_config_tx, api_config_rx) = watch::channel(Arc::new(config.clone()));
|
||||||
let (detected_ips_tx, detected_ips_rx) = watch::channel((None::<IpAddr>, None::<IpAddr>));
|
let (detected_ips_tx, detected_ips_rx) = watch::channel((None::<IpAddr>, None::<IpAddr>));
|
||||||
let initial_admission_open = !config.general.use_middle_proxy;
|
let initial_direct_first =
|
||||||
|
config.general.use_middle_proxy && config.general.me2dc_fallback;
|
||||||
|
let initial_admission_open = !config.general.use_middle_proxy || initial_direct_first;
|
||||||
let (admission_tx, admission_rx) = watch::channel(initial_admission_open);
|
let (admission_tx, admission_rx) = watch::channel(initial_admission_open);
|
||||||
let initial_route_mode = if config.general.use_middle_proxy {
|
let initial_route_mode = if !config.general.use_middle_proxy || initial_direct_first {
|
||||||
RelayRouteMode::Middle
|
|
||||||
} else {
|
|
||||||
RelayRouteMode::Direct
|
RelayRouteMode::Direct
|
||||||
|
} else {
|
||||||
|
RelayRouteMode::Middle
|
||||||
};
|
};
|
||||||
let route_runtime = Arc::new(RouteRuntimeController::new(initial_route_mode));
|
let route_runtime = Arc::new(RouteRuntimeController::new(initial_route_mode));
|
||||||
let api_me_pool = Arc::new(RwLock::new(None::<Arc<MePool>>));
|
let api_me_pool = Arc::new(RwLock::new(None::<Arc<MePool>>));
|
||||||
@@ -602,8 +604,9 @@ async fn run_telemt_core(
|
|||||||
let me_init_retry_attempts = config.general.me_init_retry_attempts;
|
let me_init_retry_attempts = config.general.me_init_retry_attempts;
|
||||||
if use_middle_proxy && !decision.ipv4_me && !decision.ipv6_me {
|
if use_middle_proxy && !decision.ipv4_me && !decision.ipv6_me {
|
||||||
if me2dc_fallback {
|
if me2dc_fallback {
|
||||||
warn!("No usable IP family for Middle Proxy detected; falling back to direct DC");
|
warn!(
|
||||||
use_middle_proxy = false;
|
"No usable IP family for Middle Proxy detected; Direct-DC startup fallback is active while ME init retries continue"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
"No usable IP family for Middle Proxy detected; me2dc_fallback=false, ME init retries stay active"
|
"No usable IP family for Middle Proxy detected; me2dc_fallback=false, ME init retries stay active"
|
||||||
@@ -665,23 +668,32 @@ async fn run_telemt_core(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (me_ready_tx, me_ready_rx) = watch::channel(0_u64);
|
let (me_ready_tx, me_ready_rx) = watch::channel(0_u64);
|
||||||
|
let direct_first_startup = use_middle_proxy && me2dc_fallback;
|
||||||
|
|
||||||
let me_pool: Option<Arc<MePool>> = me_startup::initialize_me_pool(
|
let me_pool: Option<Arc<MePool>> = if direct_first_startup {
|
||||||
use_middle_proxy,
|
None
|
||||||
&config,
|
} else {
|
||||||
&decision,
|
me_startup::initialize_me_pool(
|
||||||
&probe,
|
use_middle_proxy,
|
||||||
&startup_tracker,
|
&config,
|
||||||
upstream_manager.clone(),
|
&decision,
|
||||||
rng.clone(),
|
&probe,
|
||||||
stats.clone(),
|
&startup_tracker,
|
||||||
api_me_pool.clone(),
|
upstream_manager.clone(),
|
||||||
me_ready_tx.clone(),
|
rng.clone(),
|
||||||
)
|
stats.clone(),
|
||||||
.await;
|
api_me_pool.clone(),
|
||||||
|
me_ready_tx.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
// If ME failed to initialize, force direct-only mode.
|
// If ME failed to initialize, force direct-only mode.
|
||||||
if me_pool.is_some() {
|
if direct_first_startup {
|
||||||
|
startup_tracker.set_transport_mode("direct").await;
|
||||||
|
startup_tracker.set_degraded(true).await;
|
||||||
|
info!("Transport: Direct DC startup fallback active; Middle-End bootstrap continues in background");
|
||||||
|
} else if me_pool.is_some() {
|
||||||
startup_tracker.set_transport_mode("middle_proxy").await;
|
startup_tracker.set_transport_mode("middle_proxy").await;
|
||||||
startup_tracker.set_degraded(false).await;
|
startup_tracker.set_degraded(false).await;
|
||||||
info!("Transport: Middle-End Proxy - all DC-over-RPC");
|
info!("Transport: Middle-End Proxy - all DC-over-RPC");
|
||||||
@@ -719,18 +731,33 @@ async fn run_telemt_core(
|
|||||||
config.access.cidr_rate_limits.clone(),
|
config.access.cidr_rate_limits.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
connectivity::run_startup_connectivity(
|
if direct_first_startup {
|
||||||
&config,
|
startup_tracker
|
||||||
&me_pool,
|
.skip_component(
|
||||||
rng.clone(),
|
COMPONENT_ME_CONNECTIVITY_PING,
|
||||||
&startup_tracker,
|
Some("deferred by direct-first startup".to_string()),
|
||||||
upstream_manager.clone(),
|
)
|
||||||
prefer_ipv6,
|
.await;
|
||||||
&decision,
|
startup_tracker
|
||||||
process_started_at,
|
.skip_component(
|
||||||
api_me_pool.clone(),
|
COMPONENT_DC_CONNECTIVITY_PING,
|
||||||
)
|
Some("background health checks active".to_string()),
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
connectivity::run_startup_connectivity(
|
||||||
|
&config,
|
||||||
|
&me_pool,
|
||||||
|
rng.clone(),
|
||||||
|
&startup_tracker,
|
||||||
|
upstream_manager.clone(),
|
||||||
|
prefer_ipv6,
|
||||||
|
&decision,
|
||||||
|
process_started_at,
|
||||||
|
api_me_pool.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
let runtime_watches = runtime_tasks::spawn_runtime_tasks(
|
let runtime_watches = runtime_tasks::spawn_runtime_tasks(
|
||||||
&config,
|
&config,
|
||||||
@@ -758,9 +785,70 @@ async fn run_telemt_core(
|
|||||||
let detected_ip_v4 = runtime_watches.detected_ip_v4;
|
let detected_ip_v4 = runtime_watches.detected_ip_v4;
|
||||||
let detected_ip_v6 = runtime_watches.detected_ip_v6;
|
let detected_ip_v6 = runtime_watches.detected_ip_v6;
|
||||||
|
|
||||||
|
if direct_first_startup {
|
||||||
|
let config_bg = config.clone();
|
||||||
|
let decision_bg = decision.clone();
|
||||||
|
let probe_bg = probe.clone();
|
||||||
|
let startup_tracker_bg = startup_tracker.clone();
|
||||||
|
let upstream_manager_bg = upstream_manager.clone();
|
||||||
|
let rng_bg = rng.clone();
|
||||||
|
let stats_bg = stats.clone();
|
||||||
|
let api_me_pool_bg = api_me_pool.clone();
|
||||||
|
let me_ready_tx_bg = me_ready_tx.clone();
|
||||||
|
let config_rx_bg = config_rx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut bootstrap_attempt: u32 = 0;
|
||||||
|
loop {
|
||||||
|
bootstrap_attempt = bootstrap_attempt.saturating_add(1);
|
||||||
|
let pool = me_startup::initialize_me_pool(
|
||||||
|
true,
|
||||||
|
config_bg.as_ref(),
|
||||||
|
&decision_bg,
|
||||||
|
&probe_bg,
|
||||||
|
&startup_tracker_bg,
|
||||||
|
upstream_manager_bg.clone(),
|
||||||
|
rng_bg.clone(),
|
||||||
|
stats_bg.clone(),
|
||||||
|
api_me_pool_bg.clone(),
|
||||||
|
me_ready_tx_bg.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Some(pool) = pool {
|
||||||
|
runtime_tasks::spawn_middle_proxy_runtime_tasks(
|
||||||
|
config_bg.as_ref(),
|
||||||
|
config_rx_bg,
|
||||||
|
pool,
|
||||||
|
rng_bg,
|
||||||
|
me_ready_tx_bg,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if me_init_retry_attempts > 0 && bootstrap_attempt >= me_init_retry_attempts {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let startup_tracker_ready = startup_tracker.clone();
|
||||||
|
let api_me_pool_ready = api_me_pool.clone();
|
||||||
|
let mut me_ready_rx_transport = me_ready_tx.subscribe();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if me_ready_rx_transport.changed().await.is_ok() {
|
||||||
|
if let Some(pool) = api_me_pool_ready.read().await.as_ref() {
|
||||||
|
pool.set_runtime_ready(true);
|
||||||
|
}
|
||||||
|
startup_tracker_ready.set_transport_mode("middle_proxy").await;
|
||||||
|
startup_tracker_ready.set_degraded(false).await;
|
||||||
|
info!("Transport: Middle-End Proxy restored for new sessions");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
admission::configure_admission_gate(
|
admission::configure_admission_gate(
|
||||||
&config,
|
&config,
|
||||||
me_pool.clone(),
|
me_pool.clone(),
|
||||||
|
api_me_pool.clone(),
|
||||||
route_runtime.clone(),
|
route_runtime.clone(),
|
||||||
&admission_tx,
|
&admission_tx,
|
||||||
config_rx.clone(),
|
config_rx.clone(),
|
||||||
@@ -789,6 +877,7 @@ async fn run_telemt_core(
|
|||||||
buffer_pool.clone(),
|
buffer_pool.clone(),
|
||||||
rng.clone(),
|
rng.clone(),
|
||||||
me_pool.clone(),
|
me_pool.clone(),
|
||||||
|
api_me_pool.clone(),
|
||||||
route_runtime.clone(),
|
route_runtime.clone(),
|
||||||
tls_cache.clone(),
|
tls_cache.clone(),
|
||||||
ip_tracker.clone(),
|
ip_tracker.clone(),
|
||||||
@@ -843,6 +932,7 @@ async fn run_telemt_core(
|
|||||||
buffer_pool.clone(),
|
buffer_pool.clone(),
|
||||||
rng.clone(),
|
rng.clone(),
|
||||||
me_pool.clone(),
|
me_pool.clone(),
|
||||||
|
api_me_pool.clone(),
|
||||||
route_runtime.clone(),
|
route_runtime.clone(),
|
||||||
tls_cache.clone(),
|
tls_cache.clone(),
|
||||||
ip_tracker.clone(),
|
ip_tracker.clone(),
|
||||||
|
|||||||
@@ -257,45 +257,7 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(pool) = me_pool {
|
if let Some(pool) = me_pool {
|
||||||
let reinit_trigger_capacity = config.general.me_reinit_trigger_channel.max(1);
|
spawn_middle_proxy_runtime_tasks(config, config_rx.clone(), pool, rng, me_ready_tx);
|
||||||
let (reinit_tx, reinit_rx) = mpsc::channel::<MeReinitTrigger>(reinit_trigger_capacity);
|
|
||||||
|
|
||||||
let pool_clone_sched = pool.clone();
|
|
||||||
let rng_clone_sched = rng.clone();
|
|
||||||
let config_rx_clone_sched = config_rx.clone();
|
|
||||||
let me_ready_tx_sched = me_ready_tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
crate::transport::middle_proxy::me_reinit_scheduler(
|
|
||||||
pool_clone_sched,
|
|
||||||
rng_clone_sched,
|
|
||||||
config_rx_clone_sched,
|
|
||||||
reinit_rx,
|
|
||||||
me_ready_tx_sched,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
|
|
||||||
let pool_clone = pool.clone();
|
|
||||||
let config_rx_clone = config_rx.clone();
|
|
||||||
let reinit_tx_updater = reinit_tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
crate::transport::middle_proxy::me_config_updater(
|
|
||||||
pool_clone,
|
|
||||||
config_rx_clone,
|
|
||||||
reinit_tx_updater,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
|
|
||||||
let config_rx_clone_rot = config_rx.clone();
|
|
||||||
let reinit_tx_rotation = reinit_tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
crate::transport::middle_proxy::me_rotation_task(
|
|
||||||
config_rx_clone_rot,
|
|
||||||
reinit_tx_rotation,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimeWatches {
|
RuntimeWatches {
|
||||||
@@ -306,6 +268,51 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spawn_middle_proxy_runtime_tasks(
|
||||||
|
config: &ProxyConfig,
|
||||||
|
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||||
|
pool: Arc<MePool>,
|
||||||
|
rng: Arc<SecureRandom>,
|
||||||
|
me_ready_tx: watch::Sender<u64>,
|
||||||
|
) {
|
||||||
|
let reinit_trigger_capacity = config.general.me_reinit_trigger_channel.max(1);
|
||||||
|
let (reinit_tx, reinit_rx) = mpsc::channel::<MeReinitTrigger>(reinit_trigger_capacity);
|
||||||
|
|
||||||
|
let pool_clone_sched = pool.clone();
|
||||||
|
let rng_clone_sched = rng.clone();
|
||||||
|
let config_rx_clone_sched = config_rx.clone();
|
||||||
|
let me_ready_tx_sched = me_ready_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_reinit_scheduler(
|
||||||
|
pool_clone_sched,
|
||||||
|
rng_clone_sched,
|
||||||
|
config_rx_clone_sched,
|
||||||
|
reinit_rx,
|
||||||
|
me_ready_tx_sched,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let pool_clone = pool.clone();
|
||||||
|
let config_rx_clone = config_rx.clone();
|
||||||
|
let reinit_tx_updater = reinit_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_config_updater(
|
||||||
|
pool_clone,
|
||||||
|
config_rx_clone,
|
||||||
|
reinit_tx_updater,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let config_rx_clone_rot = config_rx.clone();
|
||||||
|
let reinit_tx_rotation = reinit_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_rotation_task(config_rx_clone_rot, reinit_tx_rotation)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn apply_runtime_log_filter(
|
pub(crate) async fn apply_runtime_log_filter(
|
||||||
has_rust_log: bool,
|
has_rust_log: bool,
|
||||||
effective_log_level: &LogLevel,
|
effective_log_level: &LogLevel,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
@@ -452,7 +453,50 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn handle_client_stream_with_shared<S>(
|
pub async fn handle_client_stream_with_shared<S>(
|
||||||
|
stream: S,
|
||||||
|
peer: SocketAddr,
|
||||||
|
config: Arc<ProxyConfig>,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
upstream_manager: Arc<UpstreamManager>,
|
||||||
|
replay_checker: Arc<ReplayChecker>,
|
||||||
|
buffer_pool: Arc<BufferPool>,
|
||||||
|
rng: Arc<SecureRandom>,
|
||||||
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
route_runtime: Arc<RouteRuntimeController>,
|
||||||
|
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||||
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
|
beobachten: Arc<BeobachtenStore>,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
|
proxy_protocol_enabled: bool,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
handle_client_stream_with_shared_and_pool_runtime(
|
||||||
|
stream,
|
||||||
|
peer,
|
||||||
|
config,
|
||||||
|
stats,
|
||||||
|
upstream_manager,
|
||||||
|
replay_checker,
|
||||||
|
buffer_pool,
|
||||||
|
rng,
|
||||||
|
me_pool,
|
||||||
|
None,
|
||||||
|
route_runtime,
|
||||||
|
tls_cache,
|
||||||
|
ip_tracker,
|
||||||
|
beobachten,
|
||||||
|
shared,
|
||||||
|
proxy_protocol_enabled,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn handle_client_stream_with_shared_and_pool_runtime<S>(
|
||||||
mut stream: S,
|
mut stream: S,
|
||||||
peer: SocketAddr,
|
peer: SocketAddr,
|
||||||
config: Arc<ProxyConfig>,
|
config: Arc<ProxyConfig>,
|
||||||
@@ -462,6 +506,7 @@ pub async fn handle_client_stream_with_shared<S>(
|
|||||||
buffer_pool: Arc<BufferPool>,
|
buffer_pool: Arc<BufferPool>,
|
||||||
rng: Arc<SecureRandom>,
|
rng: Arc<SecureRandom>,
|
||||||
me_pool: Option<Arc<MePool>>,
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
me_pool_runtime: Option<Arc<RwLock<Option<Arc<MePool>>>>>,
|
||||||
route_runtime: Arc<RouteRuntimeController>,
|
route_runtime: Arc<RouteRuntimeController>,
|
||||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
@@ -731,6 +776,7 @@ where
|
|||||||
RunningClientHandler::handle_authenticated_static_with_shared(
|
RunningClientHandler::handle_authenticated_static_with_shared(
|
||||||
crypto_reader, crypto_writer, success,
|
crypto_reader, crypto_writer, success,
|
||||||
upstream_manager, stats, config, buffer_pool, rng, me_pool,
|
upstream_manager, stats, config, buffer_pool, rng, me_pool,
|
||||||
|
me_pool_runtime,
|
||||||
route_runtime.clone(),
|
route_runtime.clone(),
|
||||||
local_addr, real_peer, ip_tracker.clone(),
|
local_addr, real_peer, ip_tracker.clone(),
|
||||||
shared.clone(),
|
shared.clone(),
|
||||||
@@ -791,6 +837,7 @@ where
|
|||||||
buffer_pool,
|
buffer_pool,
|
||||||
rng,
|
rng,
|
||||||
me_pool,
|
me_pool,
|
||||||
|
me_pool_runtime,
|
||||||
route_runtime.clone(),
|
route_runtime.clone(),
|
||||||
local_addr,
|
local_addr,
|
||||||
real_peer,
|
real_peer,
|
||||||
@@ -846,6 +893,7 @@ pub struct RunningClientHandler {
|
|||||||
buffer_pool: Arc<BufferPool>,
|
buffer_pool: Arc<BufferPool>,
|
||||||
rng: Arc<SecureRandom>,
|
rng: Arc<SecureRandom>,
|
||||||
me_pool: Option<Arc<MePool>>,
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
me_pool_runtime: Option<Arc<RwLock<Option<Arc<MePool>>>>>,
|
||||||
route_runtime: Arc<RouteRuntimeController>,
|
route_runtime: Arc<RouteRuntimeController>,
|
||||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
@@ -891,6 +939,7 @@ impl ClientHandler {
|
|||||||
buffer_pool,
|
buffer_pool,
|
||||||
rng,
|
rng,
|
||||||
me_pool,
|
me_pool,
|
||||||
|
None,
|
||||||
route_runtime,
|
route_runtime,
|
||||||
tls_cache,
|
tls_cache,
|
||||||
ip_tracker,
|
ip_tracker,
|
||||||
@@ -915,6 +964,7 @@ impl ClientHandler {
|
|||||||
buffer_pool: Arc<BufferPool>,
|
buffer_pool: Arc<BufferPool>,
|
||||||
rng: Arc<SecureRandom>,
|
rng: Arc<SecureRandom>,
|
||||||
me_pool: Option<Arc<MePool>>,
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
me_pool_runtime: Option<Arc<RwLock<Option<Arc<MePool>>>>>,
|
||||||
route_runtime: Arc<RouteRuntimeController>,
|
route_runtime: Arc<RouteRuntimeController>,
|
||||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
@@ -938,6 +988,7 @@ impl ClientHandler {
|
|||||||
buffer_pool,
|
buffer_pool,
|
||||||
rng,
|
rng,
|
||||||
me_pool,
|
me_pool,
|
||||||
|
me_pool_runtime,
|
||||||
route_runtime,
|
route_runtime,
|
||||||
tls_cache,
|
tls_cache,
|
||||||
ip_tracker,
|
ip_tracker,
|
||||||
@@ -1345,6 +1396,7 @@ impl RunningClientHandler {
|
|||||||
buffer_pool,
|
buffer_pool,
|
||||||
self.rng,
|
self.rng,
|
||||||
self.me_pool,
|
self.me_pool,
|
||||||
|
self.me_pool_runtime,
|
||||||
self.route_runtime.clone(),
|
self.route_runtime.clone(),
|
||||||
local_addr,
|
local_addr,
|
||||||
peer,
|
peer,
|
||||||
@@ -1429,6 +1481,7 @@ impl RunningClientHandler {
|
|||||||
buffer_pool,
|
buffer_pool,
|
||||||
self.rng,
|
self.rng,
|
||||||
self.me_pool,
|
self.me_pool,
|
||||||
|
self.me_pool_runtime,
|
||||||
self.route_runtime.clone(),
|
self.route_runtime.clone(),
|
||||||
local_addr,
|
local_addr,
|
||||||
peer,
|
peer,
|
||||||
@@ -1472,6 +1525,7 @@ impl RunningClientHandler {
|
|||||||
buffer_pool,
|
buffer_pool,
|
||||||
rng,
|
rng,
|
||||||
me_pool,
|
me_pool,
|
||||||
|
None,
|
||||||
route_runtime,
|
route_runtime,
|
||||||
local_addr,
|
local_addr,
|
||||||
peer_addr,
|
peer_addr,
|
||||||
@@ -1491,6 +1545,7 @@ impl RunningClientHandler {
|
|||||||
buffer_pool: Arc<BufferPool>,
|
buffer_pool: Arc<BufferPool>,
|
||||||
rng: Arc<SecureRandom>,
|
rng: Arc<SecureRandom>,
|
||||||
me_pool: Option<Arc<MePool>>,
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
me_pool_runtime: Option<Arc<RwLock<Option<Arc<MePool>>>>>,
|
||||||
route_runtime: Arc<RouteRuntimeController>,
|
route_runtime: Arc<RouteRuntimeController>,
|
||||||
local_addr: SocketAddr,
|
local_addr: SocketAddr,
|
||||||
peer_addr: SocketAddr,
|
peer_addr: SocketAddr,
|
||||||
@@ -1521,15 +1576,29 @@ impl RunningClientHandler {
|
|||||||
|
|
||||||
let route_snapshot = route_runtime.snapshot();
|
let route_snapshot = route_runtime.snapshot();
|
||||||
let session_id = rng.u64();
|
let session_id = rng.u64();
|
||||||
let relay_result = if config.general.use_middle_proxy
|
let selected_me_pool = if config.general.use_middle_proxy
|
||||||
&& matches!(route_snapshot.mode, RelayRouteMode::Middle)
|
&& matches!(route_snapshot.mode, RelayRouteMode::Middle)
|
||||||
{
|
{
|
||||||
if let Some(ref pool) = me_pool {
|
if let Some(ref pool) = me_pool {
|
||||||
|
Some(pool.clone())
|
||||||
|
} else if let Some(pool_runtime) = me_pool_runtime.as_ref() {
|
||||||
|
pool_runtime.read().await.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let relay_result = if config.general.use_middle_proxy
|
||||||
|
&& matches!(route_snapshot.mode, RelayRouteMode::Middle)
|
||||||
|
{
|
||||||
|
if let Some(pool) = selected_me_pool {
|
||||||
handle_via_middle_proxy(
|
handle_via_middle_proxy(
|
||||||
client_reader,
|
client_reader,
|
||||||
client_writer,
|
client_writer,
|
||||||
success,
|
success,
|
||||||
pool.clone(),
|
pool,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
config,
|
config,
|
||||||
buffer_pool,
|
buffer_pool,
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ struct CopyOutcome {
|
|||||||
ended_by_eof: bool,
|
ended_by_eof: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct MaskTcpTarget<'a> {
|
||||||
|
host: &'a str,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
async fn copy_with_idle_timeout<R, W>(
|
async fn copy_with_idle_timeout<R, W>(
|
||||||
reader: &mut R,
|
reader: &mut R,
|
||||||
writer: &mut W,
|
writer: &mut W,
|
||||||
@@ -331,7 +337,9 @@ async fn wait_mask_outcome_budget(started: Instant, config: &ProxyConfig) {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tls_domain_mask_host_tests {
|
mod tls_domain_mask_host_tests {
|
||||||
use super::{mask_host_for_initial_data, matching_tls_domain_for_sni};
|
use super::{
|
||||||
|
mask_host_for_initial_data, mask_tcp_target_for_initial_data, matching_tls_domain_for_sni,
|
||||||
|
};
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
|
|
||||||
fn client_hello_with_sni(sni_host: &str) -> Vec<u8> {
|
fn client_hello_with_sni(sni_host: &str) -> Vec<u8> {
|
||||||
@@ -410,6 +418,25 @@ mod tls_domain_mask_host_tests {
|
|||||||
|
|
||||||
assert_eq!(mask_host_for_initial_data(&config, &initial_data), "b.com");
|
assert_eq!(mask_host_for_initial_data(&config, &initial_data), "b.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exclusive_mask_target_overrides_only_matching_sni() {
|
||||||
|
let mut config = config_with_tls_domains();
|
||||||
|
config
|
||||||
|
.censorship
|
||||||
|
.exclusive_mask
|
||||||
|
.insert("b.com".to_string(), "origin-b.example:8443".to_string());
|
||||||
|
let b_initial_data = client_hello_with_sni("B.COM");
|
||||||
|
let c_initial_data = client_hello_with_sni("c.com");
|
||||||
|
|
||||||
|
let b_target = mask_tcp_target_for_initial_data(&config, &b_initial_data);
|
||||||
|
let c_target = mask_tcp_target_for_initial_data(&config, &c_initial_data);
|
||||||
|
|
||||||
|
assert_eq!(b_target.host, "origin-b.example");
|
||||||
|
assert_eq!(b_target.port, 8443);
|
||||||
|
assert_eq!(c_target.host, "c.com");
|
||||||
|
assert_eq!(c_target.port, config.censorship.mask_port);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect client type based on initial data
|
/// Detect client type based on initial data
|
||||||
@@ -458,7 +485,61 @@ fn matching_tls_domain_for_sni<'a>(config: &'a ProxyConfig, sni: &str) -> Option
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_exclusive_mask_target(target: &str) -> Option<MaskTcpTarget<'_>> {
|
||||||
|
let target = target.trim();
|
||||||
|
if target.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.starts_with('[') {
|
||||||
|
let end = target.find(']')?;
|
||||||
|
if target.get(end + 1..end + 2)? != ":" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let port = target[end + 2..].parse::<u16>().ok()?;
|
||||||
|
return (port > 0).then_some(MaskTcpTarget {
|
||||||
|
host: &target[..=end],
|
||||||
|
port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (host, port) = target.rsplit_once(':')?;
|
||||||
|
if host.is_empty() || host.contains(':') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let port = port.parse::<u16>().ok()?;
|
||||||
|
(port > 0).then_some(MaskTcpTarget { host, port })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exclusive_mask_target_for_sni<'a>(
|
||||||
|
config: &'a ProxyConfig,
|
||||||
|
sni: &str,
|
||||||
|
) -> Option<MaskTcpTarget<'a>> {
|
||||||
|
for (domain, target) in &config.censorship.exclusive_mask {
|
||||||
|
if domain.eq_ignore_ascii_case(sni) {
|
||||||
|
return parse_exclusive_mask_target(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn mask_host_for_initial_data<'a>(config: &'a ProxyConfig, initial_data: &[u8]) -> &'a str {
|
fn mask_host_for_initial_data<'a>(config: &'a ProxyConfig, initial_data: &[u8]) -> &'a str {
|
||||||
|
mask_tcp_target_for_initial_data(config, initial_data).host
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_tcp_target_for_initial_data<'a>(
|
||||||
|
config: &'a ProxyConfig,
|
||||||
|
initial_data: &[u8],
|
||||||
|
) -> MaskTcpTarget<'a> {
|
||||||
|
if let Some(target) = tls::extract_sni_from_client_hello(initial_data)
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|sni| exclusive_mask_target_for_sni(config, sni))
|
||||||
|
{
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
let configured_mask_host = config
|
let configured_mask_host = config
|
||||||
.censorship
|
.censorship
|
||||||
.mask_host
|
.mask_host
|
||||||
@@ -466,13 +547,20 @@ fn mask_host_for_initial_data<'a>(config: &'a ProxyConfig, initial_data: &[u8])
|
|||||||
.unwrap_or(&config.censorship.tls_domain);
|
.unwrap_or(&config.censorship.tls_domain);
|
||||||
|
|
||||||
if !configured_mask_host.eq_ignore_ascii_case(&config.censorship.tls_domain) {
|
if !configured_mask_host.eq_ignore_ascii_case(&config.censorship.tls_domain) {
|
||||||
return configured_mask_host;
|
return MaskTcpTarget {
|
||||||
|
host: configured_mask_host,
|
||||||
|
port: config.censorship.mask_port,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
tls::extract_sni_from_client_hello(initial_data)
|
let host = tls::extract_sni_from_client_hello(initial_data)
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|sni| matching_tls_domain_for_sni(config, sni))
|
.and_then(|sni| matching_tls_domain_for_sni(config, sni))
|
||||||
.unwrap_or(configured_mask_host)
|
.unwrap_or(configured_mask_host);
|
||||||
|
MaskTcpTarget {
|
||||||
|
host,
|
||||||
|
port: config.censorship.mask_port,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonical_ip(ip: IpAddr) -> IpAddr {
|
fn canonical_ip(ip: IpAddr) -> IpAddr {
|
||||||
@@ -770,9 +858,15 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let exclusive_tcp_target = tls::extract_sni_from_client_hello(initial_data)
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|sni| exclusive_mask_target_for_sni(config, sni));
|
||||||
|
|
||||||
// Connect via Unix socket or TCP
|
// Connect via Unix socket or TCP
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(ref sock_path) = config.censorship.mask_unix_sock {
|
if exclusive_tcp_target.is_none()
|
||||||
|
&& let Some(ref sock_path) = config.censorship.mask_unix_sock
|
||||||
|
{
|
||||||
let outcome_started = Instant::now();
|
let outcome_started = Instant::now();
|
||||||
let connect_started = Instant::now();
|
let connect_started = Instant::now();
|
||||||
debug!(
|
debug!(
|
||||||
@@ -849,8 +943,10 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mask_host = mask_host_for_initial_data(config, initial_data);
|
let mask_target = exclusive_tcp_target
|
||||||
let mask_port = config.censorship.mask_port;
|
.unwrap_or_else(|| mask_tcp_target_for_initial_data(config, initial_data));
|
||||||
|
let mask_host = mask_target.host;
|
||||||
|
let mask_port = mask_target.port;
|
||||||
|
|
||||||
// Fail closed when fallback points at our own listener endpoint.
|
// Fail closed when fallback points at our own listener endpoint.
|
||||||
// Self-referential masking can create recursive proxy loops under
|
// Self-referential masking can create recursive proxy loops under
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ impl MePool {
|
|||||||
.me_reconnect_max_concurrent_per_dc
|
.me_reconnect_max_concurrent_per_dc
|
||||||
.max(1) as usize;
|
.max(1) as usize;
|
||||||
let ks = self.key_selector().await;
|
let ks = self.key_selector().await;
|
||||||
|
let me_servers = self.proxy_map_v4.read().await.len();
|
||||||
|
let secret_len = self.proxy_secret.read().await.secret.len();
|
||||||
info!(
|
info!(
|
||||||
me_servers = self.proxy_map_v4.read().await.len(),
|
me_servers,
|
||||||
pool_size,
|
pool_size,
|
||||||
connect_concurrency,
|
connect_concurrency,
|
||||||
key_selector = format_args!("0x{ks:08x}"),
|
key_selector = format_args!("0x{ks:08x}"),
|
||||||
secret_len = self.proxy_secret.read().await.secret.len(),
|
secret_len,
|
||||||
"Initializing ME pool"
|
"Initializing ME pool"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user