mirror of https://github.com/telemt/telemt.git
Merge branch 'main' into feat/shadowsocks-upstream
This commit is contained in:
commit
062464175e
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.3.20"
|
version = "3.3.21"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
# Telemt Config Parameters Reference
|
||||||
|
|
||||||
|
This document lists all configuration keys accepted by `config.toml`.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> The configuration parameters detailed in this document are intended for advanced users and fine-tuning purposes. Modifying these settings without a clear understanding of their function may lead to application instability or other unexpected behavior. Please proceed with caution and at your own risk.
|
||||||
|
|
||||||
|
## Top-level keys
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| include | `String` (special directive) | Includes another TOML file with `include = "relative/or/absolute/path.toml"`; includes are processed recursively before parsing. |
|
||||||
|
| show_link | `"*" \| String[]` | Legacy top-level link visibility selector (`"*"` for all users or explicit usernames list). |
|
||||||
|
| dc_overrides | `Map<String, String[]>` | Overrides DC endpoints for non-standard DCs; key is DC id string, value is `ip:port` list. |
|
||||||
|
| default_dc | `u8` | Default DC index used for unmapped non-standard DCs. |
|
||||||
|
|
||||||
|
## [general]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| data_path | `String` | Optional runtime data directory path. |
|
||||||
|
| prefer_ipv6 | `bool` | Prefer IPv6 where applicable in runtime logic. |
|
||||||
|
| fast_mode | `bool` | Enables fast-path optimizations for traffic processing. |
|
||||||
|
| use_middle_proxy | `bool` | Enables Middle Proxy mode. |
|
||||||
|
| proxy_secret_path | `String` | Path to proxy secret binary; can be auto-downloaded if absent. |
|
||||||
|
| proxy_config_v4_cache_path | `String` | Optional cache path for raw `getProxyConfig` (IPv4) snapshot. |
|
||||||
|
| proxy_config_v6_cache_path | `String` | Optional cache path for raw `getProxyConfigV6` (IPv6) snapshot. |
|
||||||
|
| ad_tag | `String` | Global fallback ad tag (32 hex characters). |
|
||||||
|
| middle_proxy_nat_ip | `IpAddr` | Explicit public IP override for NAT environments. |
|
||||||
|
| middle_proxy_nat_probe | `bool` | Enables NAT probing for Middle Proxy KDF/public address discovery. |
|
||||||
|
| middle_proxy_nat_stun | `String` | Deprecated legacy single STUN server for NAT probing. |
|
||||||
|
| middle_proxy_nat_stun_servers | `String[]` | Deprecated legacy STUN list for NAT probing fallback. |
|
||||||
|
| stun_nat_probe_concurrency | `usize` | Maximum concurrent STUN probes during NAT detection. |
|
||||||
|
| middle_proxy_pool_size | `usize` | Target size of active Middle Proxy writer pool. |
|
||||||
|
| middle_proxy_warm_standby | `usize` | Number of warm standby Middle-End connections. |
|
||||||
|
| me_init_retry_attempts | `u32` | Startup retries for ME pool initialization (`0` means unlimited). |
|
||||||
|
| me2dc_fallback | `bool` | Allows fallback from ME mode to direct DC when ME startup fails. |
|
||||||
|
| me_keepalive_enabled | `bool` | Enables ME keepalive padding frames. |
|
||||||
|
| me_keepalive_interval_secs | `u64` | Keepalive interval in seconds. |
|
||||||
|
| me_keepalive_jitter_secs | `u64` | Keepalive jitter in seconds. |
|
||||||
|
| me_keepalive_payload_random | `bool` | Randomizes keepalive payload bytes instead of zero payload. |
|
||||||
|
| rpc_proxy_req_every | `u64` | Interval for service `RPC_PROXY_REQ` activity signals (`0` disables). |
|
||||||
|
| me_writer_cmd_channel_capacity | `usize` | Capacity of per-writer command channel. |
|
||||||
|
| me_route_channel_capacity | `usize` | Capacity of per-connection ME response route channel. |
|
||||||
|
| me_c2me_channel_capacity | `usize` | Capacity of per-client command queue (client reader -> ME sender). |
|
||||||
|
| me_reader_route_data_wait_ms | `u64` | Bounded wait for routing ME DATA to per-connection queue (`0` = no wait). |
|
||||||
|
| me_d2c_flush_batch_max_frames | `usize` | Max ME->client frames coalesced before flush. |
|
||||||
|
| me_d2c_flush_batch_max_bytes | `usize` | Max ME->client payload bytes coalesced before flush. |
|
||||||
|
| me_d2c_flush_batch_max_delay_us | `u64` | Max microsecond wait for coalescing more ME->client frames (`0` disables timed coalescing). |
|
||||||
|
| me_d2c_ack_flush_immediate | `bool` | Flushes client writer immediately after quick-ack write. |
|
||||||
|
| direct_relay_copy_buf_c2s_bytes | `usize` | Copy buffer size for client->DC direction in direct relay. |
|
||||||
|
| direct_relay_copy_buf_s2c_bytes | `usize` | Copy buffer size for DC->client direction in direct relay. |
|
||||||
|
| crypto_pending_buffer | `usize` | Max pending ciphertext buffer per client writer (bytes). |
|
||||||
|
| max_client_frame | `usize` | Maximum allowed client MTProto frame size (bytes). |
|
||||||
|
| desync_all_full | `bool` | Emits full crypto-desync forensic logs for every event. |
|
||||||
|
| beobachten | `bool` | Enables per-IP forensic observation buckets. |
|
||||||
|
| beobachten_minutes | `u64` | Retention window (minutes) for per-IP observation buckets. |
|
||||||
|
| beobachten_flush_secs | `u64` | Snapshot flush interval (seconds) for observation output file. |
|
||||||
|
| beobachten_file | `String` | Observation snapshot output file path. |
|
||||||
|
| hardswap | `bool` | Enables hard-swap generation switching for ME pool updates. |
|
||||||
|
| me_warmup_stagger_enabled | `bool` | Enables staggered warmup for extra ME writers. |
|
||||||
|
| me_warmup_step_delay_ms | `u64` | Base delay between warmup connections (ms). |
|
||||||
|
| me_warmup_step_jitter_ms | `u64` | Jitter for warmup delay (ms). |
|
||||||
|
| me_reconnect_max_concurrent_per_dc | `u32` | Max concurrent reconnect attempts per DC. |
|
||||||
|
| me_reconnect_backoff_base_ms | `u64` | Base reconnect backoff in ms. |
|
||||||
|
| me_reconnect_backoff_cap_ms | `u64` | Cap reconnect backoff in ms. |
|
||||||
|
| me_reconnect_fast_retry_count | `u32` | Number of fast retry attempts before backoff. |
|
||||||
|
| me_single_endpoint_shadow_writers | `u8` | Additional reserve writers for one-endpoint DC groups. |
|
||||||
|
| me_single_endpoint_outage_mode_enabled | `bool` | Enables aggressive outage recovery for one-endpoint DC groups. |
|
||||||
|
| me_single_endpoint_outage_disable_quarantine | `bool` | Ignores endpoint quarantine in one-endpoint outage mode. |
|
||||||
|
| me_single_endpoint_outage_backoff_min_ms | `u64` | Minimum reconnect backoff in outage mode (ms). |
|
||||||
|
| me_single_endpoint_outage_backoff_max_ms | `u64` | Maximum reconnect backoff in outage mode (ms). |
|
||||||
|
| me_single_endpoint_shadow_rotate_every_secs | `u64` | Periodic shadow writer rotation interval (`0` disables). |
|
||||||
|
| me_floor_mode | `"static" \| "adaptive"` | Writer floor policy mode. |
|
||||||
|
| me_adaptive_floor_idle_secs | `u64` | Idle time before adaptive floor may reduce one-endpoint target. |
|
||||||
|
| me_adaptive_floor_min_writers_single_endpoint | `u8` | Minimum adaptive writer target for one-endpoint DC groups. |
|
||||||
|
| me_adaptive_floor_min_writers_multi_endpoint | `u8` | Minimum adaptive writer target for multi-endpoint DC groups. |
|
||||||
|
| me_adaptive_floor_recover_grace_secs | `u64` | Grace period to hold static floor after activity. |
|
||||||
|
| me_adaptive_floor_writers_per_core_total | `u16` | Global writer budget per logical CPU core in adaptive mode. |
|
||||||
|
| me_adaptive_floor_cpu_cores_override | `u16` | Manual CPU core count override (`0` uses auto-detection). |
|
||||||
|
| me_adaptive_floor_max_extra_writers_single_per_core | `u16` | Per-core max extra writers above base floor for one-endpoint DCs. |
|
||||||
|
| me_adaptive_floor_max_extra_writers_multi_per_core | `u16` | Per-core max extra writers above base floor for multi-endpoint DCs. |
|
||||||
|
| me_adaptive_floor_max_active_writers_per_core | `u16` | Hard cap for active ME writers per logical CPU core. |
|
||||||
|
| me_adaptive_floor_max_warm_writers_per_core | `u16` | Hard cap for warm ME writers per logical CPU core. |
|
||||||
|
| me_adaptive_floor_max_active_writers_global | `u32` | Hard global cap for active ME writers. |
|
||||||
|
| me_adaptive_floor_max_warm_writers_global | `u32` | Hard global cap for warm ME writers. |
|
||||||
|
| upstream_connect_retry_attempts | `u32` | Connect attempts for selected upstream before error/fallback. |
|
||||||
|
| upstream_connect_retry_backoff_ms | `u64` | Delay between upstream connect attempts (ms). |
|
||||||
|
| upstream_connect_budget_ms | `u64` | Total wall-clock budget for one upstream connect request (ms). |
|
||||||
|
| upstream_unhealthy_fail_threshold | `u32` | Consecutive failed requests before upstream is marked unhealthy. |
|
||||||
|
| upstream_connect_failfast_hard_errors | `bool` | Skips additional retries for hard non-transient connect errors. |
|
||||||
|
| stun_iface_mismatch_ignore | `bool` | Ignores STUN/interface mismatch and keeps Middle Proxy mode. |
|
||||||
|
| unknown_dc_log_path | `String` | File path for unknown-DC request logging (`null` disables file path). |
|
||||||
|
| unknown_dc_file_log_enabled | `bool` | Enables unknown-DC file logging. |
|
||||||
|
| log_level | `"debug" \| "verbose" \| "normal" \| "silent"` | Runtime logging verbosity. |
|
||||||
|
| disable_colors | `bool` | Disables ANSI colors in logs. |
|
||||||
|
| me_socks_kdf_policy | `"strict" \| "compat"` | SOCKS-bound KDF fallback policy for ME handshake. |
|
||||||
|
| me_route_backpressure_base_timeout_ms | `u64` | Base backpressure timeout for route-channel send (ms). |
|
||||||
|
| me_route_backpressure_high_timeout_ms | `u64` | High backpressure timeout when queue occupancy exceeds watermark (ms). |
|
||||||
|
| me_route_backpressure_high_watermark_pct | `u8` | Queue occupancy threshold (%) for high timeout mode. |
|
||||||
|
| me_health_interval_ms_unhealthy | `u64` | Health monitor interval while writer coverage is degraded (ms). |
|
||||||
|
| me_health_interval_ms_healthy | `u64` | Health monitor interval while writer coverage is healthy (ms). |
|
||||||
|
| me_admission_poll_ms | `u64` | Poll interval for conditional-admission checks (ms). |
|
||||||
|
| me_warn_rate_limit_ms | `u64` | Cooldown for repetitive ME warning logs (ms). |
|
||||||
|
| me_route_no_writer_mode | `"async_recovery_failfast" \| "inline_recovery_legacy" \| "hybrid_async_persistent"` | Route behavior when no writer is immediately available. |
|
||||||
|
| me_route_no_writer_wait_ms | `u64` | Max wait in async-recovery failfast mode (ms). |
|
||||||
|
| me_route_inline_recovery_attempts | `u32` | Inline recovery attempts in legacy mode. |
|
||||||
|
| me_route_inline_recovery_wait_ms | `u64` | Max inline recovery wait in legacy mode (ms). |
|
||||||
|
| fast_mode_min_tls_record | `usize` | Minimum TLS record size when fast-mode coalescing is enabled (`0` disables). |
|
||||||
|
| update_every | `u64` | Unified interval for config/secret updater tasks. |
|
||||||
|
| me_reinit_every_secs | `u64` | Periodic ME pool reinitialization interval (seconds). |
|
||||||
|
| me_hardswap_warmup_delay_min_ms | `u64` | Minimum delay between hardswap warmup connects (ms). |
|
||||||
|
| me_hardswap_warmup_delay_max_ms | `u64` | Maximum delay between hardswap warmup connects (ms). |
|
||||||
|
| me_hardswap_warmup_extra_passes | `u8` | Additional warmup passes per hardswap cycle. |
|
||||||
|
| me_hardswap_warmup_pass_backoff_base_ms | `u64` | Base backoff between hardswap warmup passes (ms). |
|
||||||
|
| me_config_stable_snapshots | `u8` | Number of identical config snapshots required before apply. |
|
||||||
|
| me_config_apply_cooldown_secs | `u64` | Cooldown between applied ME map updates (seconds). |
|
||||||
|
| me_snapshot_require_http_2xx | `bool` | Requires 2xx HTTP responses for applying config snapshots. |
|
||||||
|
| me_snapshot_reject_empty_map | `bool` | Rejects empty config snapshots. |
|
||||||
|
| me_snapshot_min_proxy_for_lines | `u32` | Minimum parsed `proxy_for` rows required to accept snapshot. |
|
||||||
|
| proxy_secret_stable_snapshots | `u8` | Number of identical secret snapshots required before runtime rotation. |
|
||||||
|
| proxy_secret_rotate_runtime | `bool` | Enables runtime proxy-secret rotation from remote source. |
|
||||||
|
| me_secret_atomic_snapshot | `bool` | Keeps selector and secret bytes from the same snapshot atomically. |
|
||||||
|
| proxy_secret_len_max | `usize` | Maximum allowed proxy-secret length (bytes). |
|
||||||
|
| me_pool_drain_ttl_secs | `u64` | Drain TTL for stale ME writers after endpoint-map changes (seconds). |
|
||||||
|
| me_pool_drain_threshold | `u64` | Max draining stale writers before batch force-close (`0` disables threshold cleanup). |
|
||||||
|
| me_bind_stale_mode | `"never" \| "ttl" \| "always"` | Policy for new binds on stale draining writers. |
|
||||||
|
| me_bind_stale_ttl_secs | `u64` | TTL for stale bind allowance when stale mode is `ttl`. |
|
||||||
|
| me_pool_min_fresh_ratio | `f32` | Minimum desired-DC fresh coverage ratio before draining stale writers. |
|
||||||
|
| me_reinit_drain_timeout_secs | `u64` | Force-close timeout for stale writers after endpoint-map changes (`0` disables force-close). |
|
||||||
|
| proxy_secret_auto_reload_secs | `u64` | Deprecated legacy secret reload interval (fallback when `update_every` is not set). |
|
||||||
|
| proxy_config_auto_reload_secs | `u64` | Deprecated legacy config reload interval (fallback when `update_every` is not set). |
|
||||||
|
| me_reinit_singleflight | `bool` | Serializes ME reinit cycles across trigger sources. |
|
||||||
|
| me_reinit_trigger_channel | `usize` | Trigger queue capacity for reinit scheduler. |
|
||||||
|
| me_reinit_coalesce_window_ms | `u64` | Trigger coalescing window before starting reinit (ms). |
|
||||||
|
| me_deterministic_writer_sort | `bool` | Enables deterministic candidate sort for writer binding path. |
|
||||||
|
| me_writer_pick_mode | `"sorted_rr" \| "p2c"` | Writer selection mode for route bind path. |
|
||||||
|
| me_writer_pick_sample_size | `u8` | Number of candidates sampled by picker in `p2c` mode. |
|
||||||
|
| ntp_check | `bool` | Enables NTP drift check at startup. |
|
||||||
|
| ntp_servers | `String[]` | NTP servers used for drift check. |
|
||||||
|
| auto_degradation_enabled | `bool` | Enables automatic degradation from ME to direct DC. |
|
||||||
|
| degradation_min_unavailable_dc_groups | `u8` | Minimum unavailable ME DC groups required before degrading. |
|
||||||
|
|
||||||
|
## [general.modes]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| classic | `bool` | Enables classic MTProxy mode. |
|
||||||
|
| secure | `bool` | Enables secure mode. |
|
||||||
|
| tls | `bool` | Enables TLS mode. |
|
||||||
|
|
||||||
|
## [general.links]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| show | `"*" \| String[]` | Selects users whose tg:// links are shown at startup. |
|
||||||
|
| public_host | `String` | Public hostname/IP override for generated tg:// links. |
|
||||||
|
| public_port | `u16` | Public port override for generated tg:// links. |
|
||||||
|
|
||||||
|
## [general.telemetry]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| core_enabled | `bool` | Enables core hot-path telemetry counters. |
|
||||||
|
| user_enabled | `bool` | Enables per-user telemetry counters. |
|
||||||
|
| me_level | `"silent" \| "normal" \| "debug"` | Middle-End telemetry verbosity level. |
|
||||||
|
|
||||||
|
## [network]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| ipv4 | `bool` | Enables IPv4 networking. |
|
||||||
|
| ipv6 | `bool` | Enables/disables IPv6 (`null` = auto-detect availability). |
|
||||||
|
| prefer | `u8` | Preferred IP family for selection (`4` or `6`). |
|
||||||
|
| multipath | `bool` | Enables multipath behavior where supported. |
|
||||||
|
| stun_use | `bool` | Global switch for STUN probing. |
|
||||||
|
| stun_servers | `String[]` | STUN server list for public IP detection. |
|
||||||
|
| stun_tcp_fallback | `bool` | Enables TCP STUN fallback when UDP STUN is blocked. |
|
||||||
|
| http_ip_detect_urls | `String[]` | HTTP endpoints used as fallback public IP detectors. |
|
||||||
|
| cache_public_ip_path | `String` | File path for caching detected public IP. |
|
||||||
|
| dns_overrides | `String[]` | Runtime DNS overrides in `host:port:ip` format. |
|
||||||
|
|
||||||
|
## [server]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| port | `u16` | Main proxy listen port. |
|
||||||
|
| listen_addr_ipv4 | `String` | IPv4 bind address for TCP listener. |
|
||||||
|
| listen_addr_ipv6 | `String` | IPv6 bind address for TCP listener. |
|
||||||
|
| listen_unix_sock | `String` | Unix socket path for listener. |
|
||||||
|
| listen_unix_sock_perm | `String` | Unix socket permissions in octal string (e.g., `"0666"`). |
|
||||||
|
| listen_tcp | `bool` | Explicit TCP listener enable/disable override. |
|
||||||
|
| proxy_protocol | `bool` | Enables HAProxy PROXY protocol parsing on incoming client connections. |
|
||||||
|
| proxy_protocol_header_timeout_ms | `u64` | Timeout for PROXY protocol header read/parse (ms). |
|
||||||
|
| metrics_port | `u16` | Metrics endpoint port (enables metrics listener). |
|
||||||
|
| metrics_listen | `String` | Full metrics bind address (`IP:PORT`), overrides `metrics_port`. |
|
||||||
|
| metrics_whitelist | `IpNetwork[]` | CIDR whitelist for metrics endpoint access. |
|
||||||
|
| max_connections | `u32` | Max concurrent client connections (`0` = unlimited). |
|
||||||
|
|
||||||
|
## [server.api]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| enabled | `bool` | Enables control-plane REST API. |
|
||||||
|
| listen | `String` | API bind address in `IP:PORT` format. |
|
||||||
|
| whitelist | `IpNetwork[]` | CIDR whitelist allowed to access API. |
|
||||||
|
| auth_header | `String` | Exact expected `Authorization` header value (empty = disabled). |
|
||||||
|
| request_body_limit_bytes | `usize` | Maximum accepted HTTP request body size. |
|
||||||
|
| minimal_runtime_enabled | `bool` | Enables minimal runtime snapshots endpoint logic. |
|
||||||
|
| minimal_runtime_cache_ttl_ms | `u64` | Cache TTL for minimal runtime snapshots (ms; `0` disables cache). |
|
||||||
|
| runtime_edge_enabled | `bool` | Enables runtime edge endpoints. |
|
||||||
|
| runtime_edge_cache_ttl_ms | `u64` | Cache TTL for runtime edge aggregation payloads (ms). |
|
||||||
|
| runtime_edge_top_n | `usize` | Top-N size for edge connection leaderboard. |
|
||||||
|
| runtime_edge_events_capacity | `usize` | Ring-buffer capacity for runtime edge events. |
|
||||||
|
| read_only | `bool` | Rejects mutating API endpoints when enabled. |
|
||||||
|
|
||||||
|
## [[server.listeners]]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| ip | `IpAddr` | Listener bind IP. |
|
||||||
|
| announce | `String` | Public IP/domain announced in proxy links (priority over `announce_ip`). |
|
||||||
|
| announce_ip | `IpAddr` | Deprecated legacy announce IP (migrated to `announce` if needed). |
|
||||||
|
| proxy_protocol | `bool` | Per-listener override for PROXY protocol enable flag. |
|
||||||
|
| reuse_allow | `bool` | Enables `SO_REUSEPORT` for multi-instance bind sharing. |
|
||||||
|
|
||||||
|
## [timeouts]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| client_handshake | `u64` | Client handshake timeout. |
|
||||||
|
| tg_connect | `u64` | Upstream Telegram connect timeout. |
|
||||||
|
| client_keepalive | `u64` | Client keepalive timeout. |
|
||||||
|
| client_ack | `u64` | Client ACK timeout. |
|
||||||
|
| me_one_retry | `u8` | Quick ME reconnect attempts for single-address DC. |
|
||||||
|
| me_one_timeout_ms | `u64` | Timeout per quick attempt for single-address DC (ms). |
|
||||||
|
|
||||||
|
## [censorship]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| tls_domain | `String` | Primary TLS domain used in fake TLS handshake profile. |
|
||||||
|
| tls_domains | `String[]` | Additional TLS domains for generating multiple links. |
|
||||||
|
| mask | `bool` | Enables masking/fronting relay mode. |
|
||||||
|
| mask_host | `String` | Upstream mask host for TLS fronting relay. |
|
||||||
|
| mask_port | `u16` | Upstream mask port for TLS fronting relay. |
|
||||||
|
| mask_unix_sock | `String` | Unix socket path for mask backend instead of TCP host/port. |
|
||||||
|
| fake_cert_len | `usize` | Length of synthetic certificate payload when emulation data is unavailable. |
|
||||||
|
| tls_emulation | `bool` | Enables certificate/TLS behavior emulation from cached real fronts. |
|
||||||
|
| tls_front_dir | `String` | Directory path for TLS front cache storage. |
|
||||||
|
| server_hello_delay_min_ms | `u64` | Minimum server_hello delay for anti-fingerprint behavior (ms). |
|
||||||
|
| server_hello_delay_max_ms | `u64` | Maximum server_hello delay for anti-fingerprint behavior (ms). |
|
||||||
|
| tls_new_session_tickets | `u8` | Number of `NewSessionTicket` messages to emit after handshake. |
|
||||||
|
| tls_full_cert_ttl_secs | `u64` | TTL for sending full cert payload per (domain, client IP) tuple. |
|
||||||
|
| alpn_enforce | `bool` | Enforces ALPN echo behavior based on client preference. |
|
||||||
|
| mask_proxy_protocol | `u8` | PROXY protocol mode for mask backend (`0` disabled, `1` v1, `2` v2). |
|
||||||
|
|
||||||
|
## [access]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| users | `Map<String, String>` | Username -> 32-hex secret mapping. |
|
||||||
|
| user_ad_tags | `Map<String, String>` | Per-user ad tags (32 hex chars). |
|
||||||
|
| user_max_tcp_conns | `Map<String, usize>` | Per-user maximum concurrent TCP connections. |
|
||||||
|
| user_expirations | `Map<String, DateTime<Utc>>` | Per-user account expiration timestamps. |
|
||||||
|
| user_data_quota | `Map<String, u64>` | Per-user data quota limits. |
|
||||||
|
| user_max_unique_ips | `Map<String, usize>` | Per-user unique source IP limits. |
|
||||||
|
| user_max_unique_ips_global_each | `usize` | Global fallback per-user unique IP limit when no per-user override exists. |
|
||||||
|
| user_max_unique_ips_mode | `"active_window" \| "time_window" \| "combined"` | Unique source IP limit accounting mode. |
|
||||||
|
| user_max_unique_ips_window_secs | `u64` | Recent-window size for unique IP accounting (seconds). |
|
||||||
|
| replay_check_len | `usize` | Replay check storage length. |
|
||||||
|
| replay_window_secs | `u64` | Replay protection time window in seconds. |
|
||||||
|
| ignore_time_skew | `bool` | Ignores client/server timestamp skew in replay validation. |
|
||||||
|
|
||||||
|
## [[upstreams]]
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| type | `"direct" \| "socks4" \| "socks5"` | Upstream transport type selector. |
|
||||||
|
| weight | `u16` | Weighted selection coefficient for this upstream. |
|
||||||
|
| enabled | `bool` | Enables/disables this upstream entry. |
|
||||||
|
| scopes | `String` | Comma-separated scope tags for routing. |
|
||||||
|
| interface | `String` | Optional outgoing interface name (`direct`, `socks4`, `socks5`). |
|
||||||
|
| bind_addresses | `String[]` | Optional source bind addresses for `direct` upstream. |
|
||||||
|
| address | `String` | Upstream proxy address (`host:port`) for SOCKS upstreams. |
|
||||||
|
| user_id | `String` | SOCKS4 user ID (only for `type = "socks4"`). |
|
||||||
|
| username | `String` | SOCKS5 username (only for `type = "socks5"`). |
|
||||||
|
| password | `String` | SOCKS5 password (only for `type = "socks5"`). |
|
||||||
|
|
@ -238,6 +238,7 @@ pub(super) struct MeWritersSummary {
|
||||||
pub(super) available_pct: f64,
|
pub(super) available_pct: f64,
|
||||||
pub(super) required_writers: usize,
|
pub(super) required_writers: usize,
|
||||||
pub(super) alive_writers: usize,
|
pub(super) alive_writers: usize,
|
||||||
|
pub(super) coverage_ratio: f64,
|
||||||
pub(super) coverage_pct: f64,
|
pub(super) coverage_pct: f64,
|
||||||
pub(super) fresh_alive_writers: usize,
|
pub(super) fresh_alive_writers: usize,
|
||||||
pub(super) fresh_coverage_pct: f64,
|
pub(super) fresh_coverage_pct: f64,
|
||||||
|
|
@ -286,6 +287,7 @@ pub(super) struct DcStatus {
|
||||||
pub(super) floor_max: usize,
|
pub(super) floor_max: usize,
|
||||||
pub(super) floor_capped: bool,
|
pub(super) floor_capped: bool,
|
||||||
pub(super) alive_writers: usize,
|
pub(super) alive_writers: usize,
|
||||||
|
pub(super) coverage_ratio: f64,
|
||||||
pub(super) coverage_pct: f64,
|
pub(super) coverage_pct: f64,
|
||||||
pub(super) fresh_alive_writers: usize,
|
pub(super) fresh_alive_writers: usize,
|
||||||
pub(super) fresh_coverage_pct: f64,
|
pub(super) fresh_coverage_pct: f64,
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ pub(super) struct RuntimeMeQualityDcRttData {
|
||||||
pub(super) rtt_ema_ms: Option<f64>,
|
pub(super) rtt_ema_ms: Option<f64>,
|
||||||
pub(super) alive_writers: usize,
|
pub(super) alive_writers: usize,
|
||||||
pub(super) required_writers: usize,
|
pub(super) required_writers: usize,
|
||||||
|
pub(super) coverage_ratio: f64,
|
||||||
pub(super) coverage_pct: f64,
|
pub(super) coverage_pct: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,6 +390,7 @@ pub(super) async fn build_runtime_me_quality_data(shared: &ApiShared) -> Runtime
|
||||||
rtt_ema_ms: dc.rtt_ms,
|
rtt_ema_ms: dc.rtt_ms,
|
||||||
alive_writers: dc.alive_writers,
|
alive_writers: dc.alive_writers,
|
||||||
required_writers: dc.required_writers,
|
required_writers: dc.required_writers,
|
||||||
|
coverage_ratio: dc.coverage_ratio,
|
||||||
coverage_pct: dc.coverage_pct,
|
coverage_pct: dc.coverage_pct,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,7 @@ async fn get_minimal_payload_cached(
|
||||||
available_pct: status.available_pct,
|
available_pct: status.available_pct,
|
||||||
required_writers: status.required_writers,
|
required_writers: status.required_writers,
|
||||||
alive_writers: status.alive_writers,
|
alive_writers: status.alive_writers,
|
||||||
|
coverage_ratio: status.coverage_ratio,
|
||||||
coverage_pct: status.coverage_pct,
|
coverage_pct: status.coverage_pct,
|
||||||
fresh_alive_writers: status.fresh_alive_writers,
|
fresh_alive_writers: status.fresh_alive_writers,
|
||||||
fresh_coverage_pct: status.fresh_coverage_pct,
|
fresh_coverage_pct: status.fresh_coverage_pct,
|
||||||
|
|
@ -374,6 +375,7 @@ async fn get_minimal_payload_cached(
|
||||||
floor_max: entry.floor_max,
|
floor_max: entry.floor_max,
|
||||||
floor_capped: entry.floor_capped,
|
floor_capped: entry.floor_capped,
|
||||||
alive_writers: entry.alive_writers,
|
alive_writers: entry.alive_writers,
|
||||||
|
coverage_ratio: entry.coverage_ratio,
|
||||||
coverage_pct: entry.coverage_pct,
|
coverage_pct: entry.coverage_pct,
|
||||||
fresh_alive_writers: entry.fresh_alive_writers,
|
fresh_alive_writers: entry.fresh_alive_writers,
|
||||||
fresh_coverage_pct: entry.fresh_coverage_pct,
|
fresh_coverage_pct: entry.fresh_coverage_pct,
|
||||||
|
|
@ -500,6 +502,7 @@ fn disabled_me_writers(now_epoch_secs: u64, reason: &'static str) -> MeWritersDa
|
||||||
available_pct: 0.0,
|
available_pct: 0.0,
|
||||||
required_writers: 0,
|
required_writers: 0,
|
||||||
alive_writers: 0,
|
alive_writers: 0,
|
||||||
|
coverage_ratio: 0.0,
|
||||||
coverage_pct: 0.0,
|
coverage_pct: 0.0,
|
||||||
fresh_alive_writers: 0,
|
fresh_alive_writers: 0,
|
||||||
fresh_coverage_pct: 0.0,
|
fresh_coverage_pct: 0.0,
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,12 @@ impl MePool {
|
||||||
|
|
||||||
let mut missing_dc = Vec::<i32>::new();
|
let mut missing_dc = Vec::<i32>::new();
|
||||||
let mut covered = 0usize;
|
let mut covered = 0usize;
|
||||||
|
let mut total = 0usize;
|
||||||
for (dc, endpoints) in desired_by_dc {
|
for (dc, endpoints) in desired_by_dc {
|
||||||
if endpoints.is_empty() {
|
if endpoints.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
total += 1;
|
||||||
if endpoints
|
if endpoints
|
||||||
.iter()
|
.iter()
|
||||||
.any(|addr| active_writer_addrs.contains(&(*dc, *addr)))
|
.any(|addr| active_writer_addrs.contains(&(*dc, *addr)))
|
||||||
|
|
@ -85,7 +87,9 @@ impl MePool {
|
||||||
}
|
}
|
||||||
|
|
||||||
missing_dc.sort_unstable();
|
missing_dc.sort_unstable();
|
||||||
let total = desired_by_dc.len().max(1);
|
if total == 0 {
|
||||||
|
return (1.0, missing_dc);
|
||||||
|
}
|
||||||
let ratio = (covered as f32) / (total as f32);
|
let ratio = (covered as f32) / (total as f32);
|
||||||
(ratio, missing_dc)
|
(ratio, missing_dc)
|
||||||
}
|
}
|
||||||
|
|
@ -399,29 +403,21 @@ impl MePool {
|
||||||
}
|
}
|
||||||
|
|
||||||
if hardswap {
|
if hardswap {
|
||||||
let mut fresh_missing_dc = Vec::<(i32, usize, usize)>::new();
|
let fresh_writer_addrs: HashSet<(i32, SocketAddr)> = writers
|
||||||
for (dc, endpoints) in &desired_by_dc {
|
.iter()
|
||||||
if endpoints.is_empty() {
|
.filter(|w| !w.draining.load(Ordering::Relaxed))
|
||||||
continue;
|
.filter(|w| w.generation == generation)
|
||||||
}
|
.map(|w| (w.writer_dc, w.addr))
|
||||||
let required = self.required_writers_for_dc(endpoints.len());
|
.collect();
|
||||||
let fresh_count = writers
|
let (fresh_coverage_ratio, fresh_missing_dc) =
|
||||||
.iter()
|
Self::coverage_ratio(&desired_by_dc, &fresh_writer_addrs);
|
||||||
.filter(|w| !w.draining.load(Ordering::Relaxed))
|
|
||||||
.filter(|w| w.generation == generation)
|
|
||||||
.filter(|w| w.writer_dc == *dc)
|
|
||||||
.filter(|w| endpoints.contains(&w.addr))
|
|
||||||
.count();
|
|
||||||
if fresh_count < required {
|
|
||||||
fresh_missing_dc.push((*dc, fresh_count, required));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !fresh_missing_dc.is_empty() {
|
if !fresh_missing_dc.is_empty() {
|
||||||
warn!(
|
warn!(
|
||||||
previous_generation,
|
previous_generation,
|
||||||
generation,
|
generation,
|
||||||
|
fresh_coverage_ratio = format_args!("{fresh_coverage_ratio:.3}"),
|
||||||
missing_dc = ?fresh_missing_dc,
|
missing_dc = ?fresh_missing_dc,
|
||||||
"ME hardswap pending: fresh generation coverage incomplete"
|
"ME hardswap pending: fresh generation DC coverage incomplete"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -491,3 +487,61 @@ impl MePool {
|
||||||
self.zero_downtime_reinit_after_map_change(rng).await;
|
self.zero_downtime_reinit_after_map_change(rng).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
|
|
||||||
|
use super::MePool;
|
||||||
|
|
||||||
|
fn addr(octet: u8, port: u16) -> SocketAddr {
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, octet)), port)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coverage_ratio_counts_dc_coverage_not_floor() {
|
||||||
|
let dc1 = addr(1, 2001);
|
||||||
|
let dc2 = addr(2, 2002);
|
||||||
|
|
||||||
|
let mut desired_by_dc = HashMap::<i32, HashSet<SocketAddr>>::new();
|
||||||
|
desired_by_dc.insert(1, HashSet::from([dc1]));
|
||||||
|
desired_by_dc.insert(2, HashSet::from([dc2]));
|
||||||
|
|
||||||
|
let active_writer_addrs = HashSet::from([(1, dc1)]);
|
||||||
|
let (ratio, missing_dc) = MePool::coverage_ratio(&desired_by_dc, &active_writer_addrs);
|
||||||
|
|
||||||
|
assert_eq!(ratio, 0.5);
|
||||||
|
assert_eq!(missing_dc, vec![2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coverage_ratio_ignores_empty_dc_groups() {
|
||||||
|
let dc1 = addr(1, 2001);
|
||||||
|
|
||||||
|
let mut desired_by_dc = HashMap::<i32, HashSet<SocketAddr>>::new();
|
||||||
|
desired_by_dc.insert(1, HashSet::from([dc1]));
|
||||||
|
desired_by_dc.insert(2, HashSet::new());
|
||||||
|
|
||||||
|
let active_writer_addrs = HashSet::from([(1, dc1)]);
|
||||||
|
let (ratio, missing_dc) = MePool::coverage_ratio(&desired_by_dc, &active_writer_addrs);
|
||||||
|
|
||||||
|
assert_eq!(ratio, 1.0);
|
||||||
|
assert!(missing_dc.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coverage_ratio_reports_missing_dcs_sorted() {
|
||||||
|
let dc1 = addr(1, 2001);
|
||||||
|
let dc2 = addr(2, 2002);
|
||||||
|
|
||||||
|
let mut desired_by_dc = HashMap::<i32, HashSet<SocketAddr>>::new();
|
||||||
|
desired_by_dc.insert(2, HashSet::from([dc2]));
|
||||||
|
desired_by_dc.insert(1, HashSet::from([dc1]));
|
||||||
|
|
||||||
|
let (ratio, missing_dc) = MePool::coverage_ratio(&desired_by_dc, &HashSet::new());
|
||||||
|
|
||||||
|
assert_eq!(ratio, 0.0);
|
||||||
|
assert_eq!(missing_dc, vec![1, 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ pub(crate) struct MeApiDcStatusSnapshot {
|
||||||
pub floor_max: usize,
|
pub floor_max: usize,
|
||||||
pub floor_capped: bool,
|
pub floor_capped: bool,
|
||||||
pub alive_writers: usize,
|
pub alive_writers: usize,
|
||||||
|
pub coverage_ratio: f64,
|
||||||
pub coverage_pct: f64,
|
pub coverage_pct: f64,
|
||||||
pub fresh_alive_writers: usize,
|
pub fresh_alive_writers: usize,
|
||||||
pub fresh_coverage_pct: f64,
|
pub fresh_coverage_pct: f64,
|
||||||
|
|
@ -62,6 +63,7 @@ pub(crate) struct MeApiStatusSnapshot {
|
||||||
pub available_pct: f64,
|
pub available_pct: f64,
|
||||||
pub required_writers: usize,
|
pub required_writers: usize,
|
||||||
pub alive_writers: usize,
|
pub alive_writers: usize,
|
||||||
|
pub coverage_ratio: f64,
|
||||||
pub coverage_pct: f64,
|
pub coverage_pct: f64,
|
||||||
pub fresh_alive_writers: usize,
|
pub fresh_alive_writers: usize,
|
||||||
pub fresh_coverage_pct: f64,
|
pub fresh_coverage_pct: f64,
|
||||||
|
|
@ -342,6 +344,8 @@ impl MePool {
|
||||||
let mut available_endpoints = 0usize;
|
let mut available_endpoints = 0usize;
|
||||||
let mut alive_writers = 0usize;
|
let mut alive_writers = 0usize;
|
||||||
let mut fresh_alive_writers = 0usize;
|
let mut fresh_alive_writers = 0usize;
|
||||||
|
let mut coverage_ratio_dcs_total = 0usize;
|
||||||
|
let mut coverage_ratio_dcs_covered = 0usize;
|
||||||
let floor_mode = self.floor_mode();
|
let floor_mode = self.floor_mode();
|
||||||
let adaptive_cpu_cores = (self
|
let adaptive_cpu_cores = (self
|
||||||
.me_adaptive_floor_cpu_cores_effective
|
.me_adaptive_floor_cpu_cores_effective
|
||||||
|
|
@ -393,6 +397,12 @@ impl MePool {
|
||||||
available_endpoints += dc_available_endpoints;
|
available_endpoints += dc_available_endpoints;
|
||||||
alive_writers += dc_alive_writers;
|
alive_writers += dc_alive_writers;
|
||||||
fresh_alive_writers += dc_fresh_alive_writers;
|
fresh_alive_writers += dc_fresh_alive_writers;
|
||||||
|
if endpoint_count > 0 {
|
||||||
|
coverage_ratio_dcs_total += 1;
|
||||||
|
if dc_alive_writers > 0 {
|
||||||
|
coverage_ratio_dcs_covered += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dcs.push(MeApiDcStatusSnapshot {
|
dcs.push(MeApiDcStatusSnapshot {
|
||||||
dc,
|
dc,
|
||||||
|
|
@ -415,6 +425,11 @@ impl MePool {
|
||||||
floor_max,
|
floor_max,
|
||||||
floor_capped,
|
floor_capped,
|
||||||
alive_writers: dc_alive_writers,
|
alive_writers: dc_alive_writers,
|
||||||
|
coverage_ratio: if endpoint_count > 0 && dc_alive_writers > 0 {
|
||||||
|
100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
coverage_pct: ratio_pct(dc_alive_writers, dc_required_writers),
|
coverage_pct: ratio_pct(dc_alive_writers, dc_required_writers),
|
||||||
fresh_alive_writers: dc_fresh_alive_writers,
|
fresh_alive_writers: dc_fresh_alive_writers,
|
||||||
fresh_coverage_pct: ratio_pct(dc_fresh_alive_writers, dc_required_writers),
|
fresh_coverage_pct: ratio_pct(dc_fresh_alive_writers, dc_required_writers),
|
||||||
|
|
@ -431,6 +446,7 @@ impl MePool {
|
||||||
available_pct: ratio_pct(available_endpoints, configured_endpoints),
|
available_pct: ratio_pct(available_endpoints, configured_endpoints),
|
||||||
required_writers,
|
required_writers,
|
||||||
alive_writers,
|
alive_writers,
|
||||||
|
coverage_ratio: ratio_pct(coverage_ratio_dcs_covered, coverage_ratio_dcs_total),
|
||||||
coverage_pct: ratio_pct(alive_writers, required_writers),
|
coverage_pct: ratio_pct(alive_writers, required_writers),
|
||||||
fresh_alive_writers,
|
fresh_alive_writers,
|
||||||
fresh_coverage_pct: ratio_pct(fresh_alive_writers, required_writers),
|
fresh_coverage_pct: ratio_pct(fresh_alive_writers, required_writers),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,728 @@
|
||||||
|
"""
|
||||||
|
Telemt Control API Python Client
|
||||||
|
Full-coverage client for https://github.com/telemt/telemt
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
client = TelemtAPI("http://127.0.0.1:9091", auth_header="your-secret")
|
||||||
|
client.health()
|
||||||
|
client.create_user("alice", max_tcp_conns=10)
|
||||||
|
client.patch_user("alice", data_quota_bytes=1_000_000_000)
|
||||||
|
client.delete_user("alice")
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Exceptions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TememtAPIError(Exception):
|
||||||
|
"""Raised when the API returns an error envelope or a transport error."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, code: str | None = None,
|
||||||
|
http_status: int | None = None, request_id: int | None = None):
|
||||||
|
super().__init__(message)
|
||||||
|
self.code = code
|
||||||
|
self.http_status = http_status
|
||||||
|
self.request_id = request_id
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (f"TememtAPIError(message={str(self)!r}, code={self.code!r}, "
|
||||||
|
f"http_status={self.http_status}, request_id={self.request_id})")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Response wrapper
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class APIResponse:
|
||||||
|
"""Wraps a successful API response envelope."""
|
||||||
|
ok: bool
|
||||||
|
data: Any
|
||||||
|
revision: str | None = None
|
||||||
|
|
||||||
|
def __repr__(self) -> str: # pragma: no cover
|
||||||
|
return f"APIResponse(ok={self.ok}, revision={self.revision!r}, data={self.data!r})"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main client
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TememtAPI:
|
||||||
|
"""
|
||||||
|
HTTP client for the Telemt Control API.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
base_url:
|
||||||
|
Scheme + host + port, e.g. ``"http://127.0.0.1:9091"``.
|
||||||
|
Trailing slash is stripped automatically.
|
||||||
|
auth_header:
|
||||||
|
Exact value for the ``Authorization`` header.
|
||||||
|
Leave *None* when ``auth_header`` is not configured server-side.
|
||||||
|
timeout:
|
||||||
|
Socket timeout in seconds for every request (default 10).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str = "http://127.0.0.1:9091",
|
||||||
|
auth_header: str | None = None,
|
||||||
|
timeout: int = 10,
|
||||||
|
) -> None:
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.auth_header = auth_header
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Low-level HTTP helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _headers(self, extra: dict | None = None) -> dict:
|
||||||
|
h = {"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Accept": "application/json"}
|
||||||
|
if self.auth_header:
|
||||||
|
h["Authorization"] = self.auth_header
|
||||||
|
if extra:
|
||||||
|
h.update(extra)
|
||||||
|
return h
|
||||||
|
|
||||||
|
def _request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
body: dict | None = None,
|
||||||
|
if_match: str | None = None,
|
||||||
|
query: dict | None = None,
|
||||||
|
) -> APIResponse:
|
||||||
|
url = self.base_url + path
|
||||||
|
if query:
|
||||||
|
qs = "&".join(f"{k}={v}" for k, v in query.items())
|
||||||
|
url = f"{url}?{qs}"
|
||||||
|
|
||||||
|
raw_body: bytes | None = None
|
||||||
|
if body is not None:
|
||||||
|
raw_body = json.dumps(body).encode()
|
||||||
|
|
||||||
|
extra_headers: dict = {}
|
||||||
|
if if_match is not None:
|
||||||
|
extra_headers["If-Match"] = if_match
|
||||||
|
|
||||||
|
req = Request(
|
||||||
|
url,
|
||||||
|
data=raw_body,
|
||||||
|
headers=self._headers(extra_headers),
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=self.timeout) as resp:
|
||||||
|
payload = json.loads(resp.read())
|
||||||
|
except HTTPError as exc:
|
||||||
|
raw = exc.read()
|
||||||
|
try:
|
||||||
|
payload = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
raise TememtAPIError(
|
||||||
|
str(exc), http_status=exc.code
|
||||||
|
) from exc
|
||||||
|
err = payload.get("error", {})
|
||||||
|
raise TememtAPIError(
|
||||||
|
err.get("message", str(exc)),
|
||||||
|
code=err.get("code"),
|
||||||
|
http_status=exc.code,
|
||||||
|
request_id=payload.get("request_id"),
|
||||||
|
) from exc
|
||||||
|
except URLError as exc:
|
||||||
|
raise TememtAPIError(str(exc)) from exc
|
||||||
|
|
||||||
|
if not payload.get("ok"):
|
||||||
|
err = payload.get("error", {})
|
||||||
|
raise TememtAPIError(
|
||||||
|
err.get("message", "unknown error"),
|
||||||
|
code=err.get("code"),
|
||||||
|
request_id=payload.get("request_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return APIResponse(
|
||||||
|
ok=True,
|
||||||
|
data=payload.get("data"),
|
||||||
|
revision=payload.get("revision"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get(self, path: str, query: dict | None = None) -> APIResponse:
|
||||||
|
return self._request("GET", path, query=query)
|
||||||
|
|
||||||
|
def _post(self, path: str, body: dict | None = None,
|
||||||
|
if_match: str | None = None) -> APIResponse:
|
||||||
|
return self._request("POST", path, body=body, if_match=if_match)
|
||||||
|
|
||||||
|
def _patch(self, path: str, body: dict,
|
||||||
|
if_match: str | None = None) -> APIResponse:
|
||||||
|
return self._request("PATCH", path, body=body, if_match=if_match)
|
||||||
|
|
||||||
|
def _delete(self, path: str, if_match: str | None = None) -> APIResponse:
|
||||||
|
return self._request("DELETE", path, if_match=if_match)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Health & system
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def health(self) -> APIResponse:
|
||||||
|
"""GET /v1/health — liveness probe."""
|
||||||
|
return self._get("/v1/health")
|
||||||
|
|
||||||
|
def system_info(self) -> APIResponse:
|
||||||
|
"""GET /v1/system/info — binary version, uptime, config hash."""
|
||||||
|
return self._get("/v1/system/info")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Runtime gates & initialization
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def runtime_gates(self) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/gates — admission gates and startup progress."""
|
||||||
|
return self._get("/v1/runtime/gates")
|
||||||
|
|
||||||
|
def runtime_initialization(self) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/initialization — detailed startup timeline."""
|
||||||
|
return self._get("/v1/runtime/initialization")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Limits & security
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def limits_effective(self) -> APIResponse:
|
||||||
|
"""GET /v1/limits/effective — effective timeout/upstream/ME limits."""
|
||||||
|
return self._get("/v1/limits/effective")
|
||||||
|
|
||||||
|
def security_posture(self) -> APIResponse:
|
||||||
|
"""GET /v1/security/posture — API auth, telemetry, log-level summary."""
|
||||||
|
return self._get("/v1/security/posture")
|
||||||
|
|
||||||
|
def security_whitelist(self) -> APIResponse:
|
||||||
|
"""GET /v1/security/whitelist — current IP whitelist CIDRs."""
|
||||||
|
return self._get("/v1/security/whitelist")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Stats
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def stats_summary(self) -> APIResponse:
|
||||||
|
"""GET /v1/stats/summary — uptime, connection totals, user count."""
|
||||||
|
return self._get("/v1/stats/summary")
|
||||||
|
|
||||||
|
def stats_zero_all(self) -> APIResponse:
|
||||||
|
"""GET /v1/stats/zero/all — zero-cost counters (core, upstream, ME, pool, desync)."""
|
||||||
|
return self._get("/v1/stats/zero/all")
|
||||||
|
|
||||||
|
def stats_upstreams(self) -> APIResponse:
|
||||||
|
"""GET /v1/stats/upstreams — upstream health + zero counters."""
|
||||||
|
return self._get("/v1/stats/upstreams")
|
||||||
|
|
||||||
|
def stats_minimal_all(self) -> APIResponse:
|
||||||
|
"""GET /v1/stats/minimal/all — ME writers + DC snapshot (requires minimal_runtime_enabled)."""
|
||||||
|
return self._get("/v1/stats/minimal/all")
|
||||||
|
|
||||||
|
def stats_me_writers(self) -> APIResponse:
|
||||||
|
"""GET /v1/stats/me-writers — per-writer ME status (requires minimal_runtime_enabled)."""
|
||||||
|
return self._get("/v1/stats/me-writers")
|
||||||
|
|
||||||
|
def stats_dcs(self) -> APIResponse:
|
||||||
|
"""GET /v1/stats/dcs — per-DC coverage and writer counts (requires minimal_runtime_enabled)."""
|
||||||
|
return self._get("/v1/stats/dcs")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Runtime deep-dive
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def runtime_me_pool_state(self) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/me_pool_state — ME pool generation/writer/refill snapshot."""
|
||||||
|
return self._get("/v1/runtime/me_pool_state")
|
||||||
|
|
||||||
|
def runtime_me_quality(self) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/me_quality — ME KDF, route-drop, and per-DC RTT counters."""
|
||||||
|
return self._get("/v1/runtime/me_quality")
|
||||||
|
|
||||||
|
def runtime_upstream_quality(self) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/upstream_quality — per-upstream health, latency, DC preferences."""
|
||||||
|
return self._get("/v1/runtime/upstream_quality")
|
||||||
|
|
||||||
|
def runtime_nat_stun(self) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/nat_stun — NAT probe state, STUN servers, reflected IPs."""
|
||||||
|
return self._get("/v1/runtime/nat_stun")
|
||||||
|
|
||||||
|
def runtime_me_selftest(self) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/me-selftest — KDF/timeskew/IP/PID/BND health state."""
|
||||||
|
return self._get("/v1/runtime/me-selftest")
|
||||||
|
|
||||||
|
def runtime_connections_summary(self) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/connections/summary — live connection totals + top-N users (requires runtime_edge_enabled)."""
|
||||||
|
return self._get("/v1/runtime/connections/summary")
|
||||||
|
|
||||||
|
def runtime_events_recent(self, limit: int | None = None) -> APIResponse:
|
||||||
|
"""GET /v1/runtime/events/recent — recent ring-buffer events (requires runtime_edge_enabled).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
limit:
|
||||||
|
Optional cap on returned events (1–1000, server default 50).
|
||||||
|
"""
|
||||||
|
query = {"limit": str(limit)} if limit is not None else None
|
||||||
|
return self._get("/v1/runtime/events/recent", query=query)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Users (read)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def list_users(self) -> APIResponse:
|
||||||
|
"""GET /v1/users — list all users with connection/traffic info."""
|
||||||
|
return self._get("/v1/users")
|
||||||
|
|
||||||
|
def get_user(self, username: str) -> APIResponse:
|
||||||
|
"""GET /v1/users/{username} — single user info."""
|
||||||
|
return self._get(f"/v1/users/{_safe(username)}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Users (write)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def create_user(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
*,
|
||||||
|
secret: str | None = None,
|
||||||
|
user_ad_tag: str | None = None,
|
||||||
|
max_tcp_conns: int | None = None,
|
||||||
|
expiration_rfc3339: str | None = None,
|
||||||
|
data_quota_bytes: int | None = None,
|
||||||
|
max_unique_ips: int | None = None,
|
||||||
|
if_match: str | None = None,
|
||||||
|
) -> APIResponse:
|
||||||
|
"""POST /v1/users — create a new user.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
username:
|
||||||
|
``[A-Za-z0-9_.-]``, length 1–64.
|
||||||
|
secret:
|
||||||
|
Exactly 32 hex chars. Auto-generated if omitted.
|
||||||
|
user_ad_tag:
|
||||||
|
Exactly 32 hex chars.
|
||||||
|
max_tcp_conns:
|
||||||
|
Per-user concurrent TCP limit.
|
||||||
|
expiration_rfc3339:
|
||||||
|
RFC3339 expiration timestamp, e.g. ``"2025-12-31T23:59:59Z"``.
|
||||||
|
data_quota_bytes:
|
||||||
|
Per-user traffic quota in bytes.
|
||||||
|
max_unique_ips:
|
||||||
|
Per-user unique source IP limit.
|
||||||
|
if_match:
|
||||||
|
Optional ``If-Match`` revision for optimistic concurrency.
|
||||||
|
"""
|
||||||
|
body: Dict[str, Any] = {"username": username}
|
||||||
|
_opt(body, "secret", secret)
|
||||||
|
_opt(body, "user_ad_tag", user_ad_tag)
|
||||||
|
_opt(body, "max_tcp_conns", max_tcp_conns)
|
||||||
|
_opt(body, "expiration_rfc3339", expiration_rfc3339)
|
||||||
|
_opt(body, "data_quota_bytes", data_quota_bytes)
|
||||||
|
_opt(body, "max_unique_ips", max_unique_ips)
|
||||||
|
return self._post("/v1/users", body=body, if_match=if_match)
|
||||||
|
|
||||||
|
def patch_user(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
*,
|
||||||
|
secret: str | None = None,
|
||||||
|
user_ad_tag: str | None = None,
|
||||||
|
max_tcp_conns: int | None = None,
|
||||||
|
expiration_rfc3339: str | None = None,
|
||||||
|
data_quota_bytes: int | None = None,
|
||||||
|
max_unique_ips: int | None = None,
|
||||||
|
if_match: str | None = None,
|
||||||
|
) -> APIResponse:
|
||||||
|
"""PATCH /v1/users/{username} — partial update; only provided fields change.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
username:
|
||||||
|
Existing username to update.
|
||||||
|
secret:
|
||||||
|
New secret (32 hex chars).
|
||||||
|
user_ad_tag:
|
||||||
|
New ad tag (32 hex chars).
|
||||||
|
max_tcp_conns:
|
||||||
|
New TCP concurrency limit.
|
||||||
|
expiration_rfc3339:
|
||||||
|
New expiration timestamp.
|
||||||
|
data_quota_bytes:
|
||||||
|
New quota in bytes.
|
||||||
|
max_unique_ips:
|
||||||
|
New unique IP limit.
|
||||||
|
if_match:
|
||||||
|
Optional ``If-Match`` revision.
|
||||||
|
"""
|
||||||
|
body: Dict[str, Any] = {}
|
||||||
|
_opt(body, "secret", secret)
|
||||||
|
_opt(body, "user_ad_tag", user_ad_tag)
|
||||||
|
_opt(body, "max_tcp_conns", max_tcp_conns)
|
||||||
|
_opt(body, "expiration_rfc3339", expiration_rfc3339)
|
||||||
|
_opt(body, "data_quota_bytes", data_quota_bytes)
|
||||||
|
_opt(body, "max_unique_ips", max_unique_ips)
|
||||||
|
if not body:
|
||||||
|
raise ValueError("patch_user: at least one field must be provided")
|
||||||
|
return self._patch(f"/v1/users/{_safe(username)}", body=body,
|
||||||
|
if_match=if_match)
|
||||||
|
|
||||||
|
def delete_user(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
*,
|
||||||
|
if_match: str | None = None,
|
||||||
|
) -> APIResponse:
|
||||||
|
"""DELETE /v1/users/{username} — remove user; blocks deletion of last user.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
if_match:
|
||||||
|
Optional ``If-Match`` revision for optimistic concurrency.
|
||||||
|
"""
|
||||||
|
return self._delete(f"/v1/users/{_safe(username)}", if_match=if_match)
|
||||||
|
|
||||||
|
# NOTE: POST /v1/users/{username}/rotate-secret currently returns 404
|
||||||
|
# in the route matcher (documented limitation). The method is provided
|
||||||
|
# for completeness and future compatibility.
|
||||||
|
def rotate_secret(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
*,
|
||||||
|
secret: str | None = None,
|
||||||
|
if_match: str | None = None,
|
||||||
|
) -> APIResponse:
|
||||||
|
"""POST /v1/users/{username}/rotate-secret — rotate user secret.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
This endpoint currently returns ``404 not_found`` in all released
|
||||||
|
versions (documented route matcher limitation). The method is
|
||||||
|
included for future compatibility.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
secret:
|
||||||
|
New secret (32 hex chars). Auto-generated if omitted.
|
||||||
|
"""
|
||||||
|
body: Dict[str, Any] = {}
|
||||||
|
_opt(body, "secret", secret)
|
||||||
|
return self._post(f"/v1/users/{_safe(username)}/rotate-secret",
|
||||||
|
body=body or None, if_match=if_match)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Convenience helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_secret() -> str:
|
||||||
|
"""Generate a random 32-character hex secret suitable for user creation."""
|
||||||
|
return secrets.token_hex(16) # 16 bytes → 32 hex chars
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Internal helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _safe(username: str) -> str:
|
||||||
|
"""Minimal guard: reject obvious path-injection attempts."""
|
||||||
|
if "/" in username or "\\" in username:
|
||||||
|
raise ValueError(f"Invalid username: {username!r}")
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
def _opt(d: dict, key: str, value: Any) -> None:
|
||||||
|
"""Add key to dict only when value is not None."""
|
||||||
|
if value is not None:
|
||||||
|
d[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CLI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _print(resp: APIResponse) -> None:
|
||||||
|
print(json.dumps(resp.data, indent=2))
|
||||||
|
if resp.revision:
|
||||||
|
print(f"# revision: {resp.revision}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_parser():
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
p = argparse.ArgumentParser(
|
||||||
|
prog="telemt_api.py",
|
||||||
|
description="Telemt Control API CLI",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
COMMANDS (read)
|
||||||
|
health Liveness check
|
||||||
|
info System info (version, uptime, config hash)
|
||||||
|
status Runtime gates + startup progress
|
||||||
|
init Runtime initialization timeline
|
||||||
|
limits Effective limits (timeouts, upstream, ME)
|
||||||
|
posture Security posture summary
|
||||||
|
whitelist IP whitelist entries
|
||||||
|
summary Stats summary (conns, uptime, users)
|
||||||
|
zero Zero-cost counters (core/upstream/ME/pool/desync)
|
||||||
|
upstreams Upstream health + zero counters
|
||||||
|
minimal ME writers + DC snapshot [minimal_runtime_enabled]
|
||||||
|
me-writers Per-writer ME status [minimal_runtime_enabled]
|
||||||
|
dcs Per-DC coverage [minimal_runtime_enabled]
|
||||||
|
me-pool ME pool generation/writer/refill snapshot
|
||||||
|
me-quality ME KDF, route-drops, per-DC RTT
|
||||||
|
upstream-quality Per-upstream health + latency
|
||||||
|
nat-stun NAT probe state + STUN servers
|
||||||
|
me-selftest KDF/timeskew/IP/PID/BND health
|
||||||
|
connections Live connection totals + top-N [runtime_edge_enabled]
|
||||||
|
events [--limit N] Recent ring-buffer events [runtime_edge_enabled]
|
||||||
|
|
||||||
|
COMMANDS (users)
|
||||||
|
users List all users
|
||||||
|
user <username> Get single user
|
||||||
|
create <username> [OPTIONS] Create user
|
||||||
|
patch <username> [OPTIONS] Partial update user
|
||||||
|
delete <username> Delete user
|
||||||
|
secret <username> [--secret S] Rotate secret (reserved; returns 404 in current release)
|
||||||
|
gen-secret Print a random 32-hex secret and exit
|
||||||
|
|
||||||
|
USER OPTIONS (for create / patch)
|
||||||
|
--secret S 32 hex chars
|
||||||
|
--ad-tag S 32 hex chars (ad tag)
|
||||||
|
--max-conns N Max concurrent TCP connections
|
||||||
|
--expires DATETIME RFC3339 expiration (e.g. 2026-12-31T23:59:59Z)
|
||||||
|
--quota N Data quota in bytes
|
||||||
|
--max-ips N Max unique source IPs
|
||||||
|
|
||||||
|
EXAMPLES
|
||||||
|
telemt_api.py health
|
||||||
|
telemt_api.py -u http://10.0.0.1:9091 -a mysecret users
|
||||||
|
telemt_api.py create alice --max-conns 5 --quota 10000000000
|
||||||
|
telemt_api.py patch alice --expires 2027-01-01T00:00:00Z
|
||||||
|
telemt_api.py delete alice
|
||||||
|
telemt_api.py events --limit 20
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
p.add_argument("-u", "--url", default="http://127.0.0.1:9091",
|
||||||
|
metavar="URL", help="API base URL (default: http://127.0.0.1:9091)")
|
||||||
|
p.add_argument("-a", "--auth", default=None, metavar="TOKEN",
|
||||||
|
help="Authorization header value")
|
||||||
|
p.add_argument("-t", "--timeout", type=int, default=10, metavar="SEC",
|
||||||
|
help="Request timeout in seconds (default: 10)")
|
||||||
|
|
||||||
|
p.add_argument("command", nargs="?", default="help",
|
||||||
|
help="Command to run (see COMMANDS below)")
|
||||||
|
p.add_argument("arg", nargs="?", default=None, metavar="USERNAME",
|
||||||
|
help="Username for user commands")
|
||||||
|
|
||||||
|
# user create/patch fields
|
||||||
|
p.add_argument("--secret", default=None)
|
||||||
|
p.add_argument("--ad-tag", dest="ad_tag", default=None)
|
||||||
|
p.add_argument("--max-conns", dest="max_conns", type=int, default=None)
|
||||||
|
p.add_argument("--expires", default=None)
|
||||||
|
p.add_argument("--quota", type=int, default=None)
|
||||||
|
p.add_argument("--max-ips", dest="max_ips", type=int, default=None)
|
||||||
|
|
||||||
|
# events
|
||||||
|
p.add_argument("--limit", type=int, default=None,
|
||||||
|
help="Max events for `events` command")
|
||||||
|
|
||||||
|
# optimistic concurrency
|
||||||
|
p.add_argument("--if-match", dest="if_match", default=None,
|
||||||
|
metavar="REVISION", help="If-Match revision header")
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
parser = _build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
cmd = (args.command or "help").lower()
|
||||||
|
|
||||||
|
if cmd in ("help", "--help"):
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if cmd == "gen-secret":
|
||||||
|
print(TememtAPI.generate_secret())
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
api = TememtAPI(args.url, auth_header=args.auth, timeout=args.timeout)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# -- read endpoints --------------------------------------------------
|
||||||
|
if cmd == "health":
|
||||||
|
_print(api.health())
|
||||||
|
|
||||||
|
elif cmd == "info":
|
||||||
|
_print(api.system_info())
|
||||||
|
|
||||||
|
elif cmd == "status":
|
||||||
|
_print(api.runtime_gates())
|
||||||
|
|
||||||
|
elif cmd == "init":
|
||||||
|
_print(api.runtime_initialization())
|
||||||
|
|
||||||
|
elif cmd == "limits":
|
||||||
|
_print(api.limits_effective())
|
||||||
|
|
||||||
|
elif cmd == "posture":
|
||||||
|
_print(api.security_posture())
|
||||||
|
|
||||||
|
elif cmd == "whitelist":
|
||||||
|
_print(api.security_whitelist())
|
||||||
|
|
||||||
|
elif cmd == "summary":
|
||||||
|
_print(api.stats_summary())
|
||||||
|
|
||||||
|
elif cmd == "zero":
|
||||||
|
_print(api.stats_zero_all())
|
||||||
|
|
||||||
|
elif cmd == "upstreams":
|
||||||
|
_print(api.stats_upstreams())
|
||||||
|
|
||||||
|
elif cmd == "minimal":
|
||||||
|
_print(api.stats_minimal_all())
|
||||||
|
|
||||||
|
elif cmd == "me-writers":
|
||||||
|
_print(api.stats_me_writers())
|
||||||
|
|
||||||
|
elif cmd == "dcs":
|
||||||
|
_print(api.stats_dcs())
|
||||||
|
|
||||||
|
elif cmd == "me-pool":
|
||||||
|
_print(api.runtime_me_pool_state())
|
||||||
|
|
||||||
|
elif cmd == "me-quality":
|
||||||
|
_print(api.runtime_me_quality())
|
||||||
|
|
||||||
|
elif cmd == "upstream-quality":
|
||||||
|
_print(api.runtime_upstream_quality())
|
||||||
|
|
||||||
|
elif cmd == "nat-stun":
|
||||||
|
_print(api.runtime_nat_stun())
|
||||||
|
|
||||||
|
elif cmd == "me-selftest":
|
||||||
|
_print(api.runtime_me_selftest())
|
||||||
|
|
||||||
|
elif cmd == "connections":
|
||||||
|
_print(api.runtime_connections_summary())
|
||||||
|
|
||||||
|
elif cmd == "events":
|
||||||
|
_print(api.runtime_events_recent(limit=args.limit))
|
||||||
|
|
||||||
|
# -- user read -------------------------------------------------------
|
||||||
|
elif cmd == "users":
|
||||||
|
resp = api.list_users()
|
||||||
|
users = resp.data or []
|
||||||
|
if not users:
|
||||||
|
print("No users configured.")
|
||||||
|
else:
|
||||||
|
fmt = "{:<24} {:>7} {:>14} {}"
|
||||||
|
print(fmt.format("USERNAME", "CONNS", "OCTETS", "LINKS"))
|
||||||
|
print("-" * 72)
|
||||||
|
for u in users:
|
||||||
|
links = (u.get("links") or {})
|
||||||
|
all_links = (links.get("classic") or []) + \
|
||||||
|
(links.get("secure") or []) + \
|
||||||
|
(links.get("tls") or [])
|
||||||
|
link_str = all_links[0] if all_links else "-"
|
||||||
|
print(fmt.format(
|
||||||
|
u["username"],
|
||||||
|
u.get("current_connections", 0),
|
||||||
|
u.get("total_octets", 0),
|
||||||
|
link_str,
|
||||||
|
))
|
||||||
|
if resp.revision:
|
||||||
|
print(f"# revision: {resp.revision}")
|
||||||
|
|
||||||
|
elif cmd == "user":
|
||||||
|
if not args.arg:
|
||||||
|
parser.error("user command requires <username>")
|
||||||
|
_print(api.get_user(args.arg))
|
||||||
|
|
||||||
|
# -- user write ------------------------------------------------------
|
||||||
|
elif cmd == "create":
|
||||||
|
if not args.arg:
|
||||||
|
parser.error("create command requires <username>")
|
||||||
|
resp = api.create_user(
|
||||||
|
args.arg,
|
||||||
|
secret=args.secret,
|
||||||
|
user_ad_tag=args.ad_tag,
|
||||||
|
max_tcp_conns=args.max_conns,
|
||||||
|
expiration_rfc3339=args.expires,
|
||||||
|
data_quota_bytes=args.quota,
|
||||||
|
max_unique_ips=args.max_ips,
|
||||||
|
if_match=args.if_match,
|
||||||
|
)
|
||||||
|
d = resp.data or {}
|
||||||
|
print(f"Created: {d.get('user', {}).get('username')}")
|
||||||
|
print(f"Secret: {d.get('secret')}")
|
||||||
|
links = (d.get("user") or {}).get("links") or {}
|
||||||
|
for kind, lst in links.items():
|
||||||
|
for link in (lst or []):
|
||||||
|
print(f"Link ({kind}): {link}")
|
||||||
|
if resp.revision:
|
||||||
|
print(f"# revision: {resp.revision}")
|
||||||
|
|
||||||
|
elif cmd == "patch":
|
||||||
|
if not args.arg:
|
||||||
|
parser.error("patch command requires <username>")
|
||||||
|
if not any([args.secret, args.ad_tag, args.max_conns,
|
||||||
|
args.expires, args.quota, args.max_ips]):
|
||||||
|
parser.error("patch requires at least one field (--secret, --max-conns, --expires, --quota, --max-ips, --ad-tag)")
|
||||||
|
_print(api.patch_user(
|
||||||
|
args.arg,
|
||||||
|
secret=args.secret,
|
||||||
|
user_ad_tag=args.ad_tag,
|
||||||
|
max_tcp_conns=args.max_conns,
|
||||||
|
expiration_rfc3339=args.expires,
|
||||||
|
data_quota_bytes=args.quota,
|
||||||
|
max_unique_ips=args.max_ips,
|
||||||
|
if_match=args.if_match,
|
||||||
|
))
|
||||||
|
|
||||||
|
elif cmd == "delete":
|
||||||
|
if not args.arg:
|
||||||
|
parser.error("delete command requires <username>")
|
||||||
|
resp = api.delete_user(args.arg, if_match=args.if_match)
|
||||||
|
print(f"Deleted: {resp.data}")
|
||||||
|
if resp.revision:
|
||||||
|
print(f"# revision: {resp.revision}")
|
||||||
|
|
||||||
|
elif cmd == "secret":
|
||||||
|
if not args.arg:
|
||||||
|
parser.error("secret command requires <username>")
|
||||||
|
_print(api.rotate_secret(args.arg, secret=args.secret,
|
||||||
|
if_match=args.if_match))
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {cmd!r}\nRun with 'help' to see available commands.",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except TememtAPIError as exc:
|
||||||
|
print(f"API error [{exc.http_status}] {exc.code}: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(130)
|
||||||
Loading…
Reference in New Issue