mirror of
https://github.com/telemt/telemt.git
synced 2026-05-13 15:21:44 +03:00
Merge branch 'flow' into user_check
This commit is contained in:
@@ -128,7 +128,48 @@ Recommended for cleaner testing:
|
||||
|
||||
Persisted cache artifacts are useful, but they are not required if packet captures already demonstrate the runtime result.
|
||||
|
||||
### 4. Capture a direct-origin trace
|
||||
### 4. Check TLS-front profile health metrics
|
||||
|
||||
If the metrics endpoint is enabled, check the TLS-front profile health before packet-capture validation:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:9999/metrics | grep -E 'telemt_tls_front_profile|telemt_tls_fetch_profile_cache|telemt_tls_front_full_cert'
|
||||
```
|
||||
|
||||
The profile-health metrics expose the runtime state of configured TLS front domains:
|
||||
|
||||
- `telemt_tls_front_profile_domains` shows configured, emitted, and suppressed domain series.
|
||||
- `telemt_tls_front_profile_info` shows profile source and feature flags per domain.
|
||||
- `telemt_tls_front_profile_age_seconds` shows cached profile age.
|
||||
- `telemt_tls_front_profile_app_data_records` shows cached AppData record count.
|
||||
- `telemt_tls_front_profile_ticket_records` shows cached ticket-like tail record count.
|
||||
- `telemt_tls_front_profile_change_cipher_spec_records` shows cached ChangeCipherSpec count.
|
||||
- `telemt_tls_front_profile_app_data_bytes` shows total cached AppData bytes.
|
||||
|
||||
Interpretation:
|
||||
|
||||
- `source="merged"` or `source="raw"` means real TLS profile data is being used.
|
||||
- `source="default"` or `is_default="true"` means the domain currently uses the synthetic default fallback.
|
||||
- `has_cert_payload="true"` means certificate payload data is available for TLS emulation.
|
||||
- Non-zero AppData/ticket/CCS counters show captured server-flight shape.
|
||||
|
||||
Example healthy output:
|
||||
|
||||
```text
|
||||
telemt_tls_front_profile_domains{status="configured"} 1
|
||||
telemt_tls_front_profile_domains{status="emitted"} 1
|
||||
telemt_tls_front_profile_domains{status="suppressed"} 0
|
||||
telemt_tls_front_profile_info{domain="itunes.apple.com",source="merged",is_default="false",has_cert_info="true",has_cert_payload="true"} 1
|
||||
telemt_tls_front_profile_age_seconds{domain="itunes.apple.com"} 20
|
||||
telemt_tls_front_profile_app_data_records{domain="itunes.apple.com"} 3
|
||||
telemt_tls_front_profile_ticket_records{domain="itunes.apple.com"} 1
|
||||
telemt_tls_front_profile_change_cipher_spec_records{domain="itunes.apple.com"} 1
|
||||
telemt_tls_front_profile_app_data_bytes{domain="itunes.apple.com"} 5240
|
||||
```
|
||||
|
||||
These metrics do not prove byte-level origin equivalence. They are an operational health signal that the configured domain is backed by real cached profile data instead of default fallback data.
|
||||
|
||||
### 5. Capture a direct-origin trace
|
||||
|
||||
From a separate client host, connect directly to the origin:
|
||||
|
||||
@@ -142,7 +183,7 @@ Capture with:
|
||||
sudo tcpdump -i any -w origin-direct.pcap host ORIGIN_IP and port 443
|
||||
```
|
||||
|
||||
### 5. Capture a Telemt FakeTLS success-path trace
|
||||
### 6. Capture a Telemt FakeTLS success-path trace
|
||||
|
||||
Now connect to Telemt with a real Telegram client through an `ee` proxy link that targets the Telemt instance.
|
||||
|
||||
@@ -154,7 +195,7 @@ Capture with:
|
||||
sudo tcpdump -i any -w telemt-emulated.pcap host TELEMT_IP and port 443
|
||||
```
|
||||
|
||||
### 6. Decode TLS record structure
|
||||
### 7. Decode TLS record structure
|
||||
|
||||
Use `tshark` to print record-level structure:
|
||||
|
||||
@@ -182,7 +223,7 @@ Focus on the server flight after ClientHello:
|
||||
- `20` = ChangeCipherSpec
|
||||
- `23` = ApplicationData
|
||||
|
||||
### 7. Build a comparison table
|
||||
### 8. Build a comparison table
|
||||
|
||||
A compact table like the following is usually enough:
|
||||
|
||||
|
||||
@@ -126,9 +126,50 @@ openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
|
||||
2. Дайте ему получить TLS front profile data для выбранного домена.
|
||||
3. Если `tls_front_dir` хранится persistently, убедитесь, что TLS front cache заполнен.
|
||||
|
||||
Persisted cache artifacts полезны, но не обязательны, если packet capture уже показывают runtime result.
|
||||
Сохранённые артефакты кэша полезны, но не обязательны, если packet capture уже показывает результат в runtime.
|
||||
|
||||
### 4. Снять direct-origin trace
|
||||
### 4. Проверить метрики состояния TLS-front profile
|
||||
|
||||
Если endpoint метрик включён, перед проверкой через packet capture можно быстро проверить состояние TLS-front profile:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:9999/metrics | grep -E 'telemt_tls_front_profile|telemt_tls_fetch_profile_cache|telemt_tls_front_full_cert'
|
||||
```
|
||||
|
||||
Метрики состояния профиля показывают runtime-состояние настроенных TLS-front доменов:
|
||||
|
||||
- `telemt_tls_front_profile_domains` показывает количество настроенных, экспортируемых и скрытых из-за лимита доменов.
|
||||
- `telemt_tls_front_profile_info` показывает источник профиля и флаги доступных данных по каждому домену.
|
||||
- `telemt_tls_front_profile_age_seconds` показывает возраст закешированного профиля.
|
||||
- `telemt_tls_front_profile_app_data_records` показывает количество закешированных AppData records.
|
||||
- `telemt_tls_front_profile_ticket_records` показывает количество закешированных ticket-like tail records.
|
||||
- `telemt_tls_front_profile_change_cipher_spec_records` показывает закешированное количество ChangeCipherSpec records.
|
||||
- `telemt_tls_front_profile_app_data_bytes` показывает общий размер закешированных AppData bytes.
|
||||
|
||||
Интерпретация:
|
||||
|
||||
- `source="merged"` или `source="raw"` означает, что используются реальные данные TLS-профиля.
|
||||
- `source="default"` или `is_default="true"` означает, что домен сейчас работает на synthetic default fallback.
|
||||
- `has_cert_payload="true"` означает, что certificate payload доступен для TLS emulation.
|
||||
- Ненулевые AppData/ticket/CCS counters показывают захваченную форму server flight.
|
||||
|
||||
Пример здорового состояния:
|
||||
|
||||
```text
|
||||
telemt_tls_front_profile_domains{status="configured"} 1
|
||||
telemt_tls_front_profile_domains{status="emitted"} 1
|
||||
telemt_tls_front_profile_domains{status="suppressed"} 0
|
||||
telemt_tls_front_profile_info{domain="itunes.apple.com",source="merged",is_default="false",has_cert_info="true",has_cert_payload="true"} 1
|
||||
telemt_tls_front_profile_age_seconds{domain="itunes.apple.com"} 20
|
||||
telemt_tls_front_profile_app_data_records{domain="itunes.apple.com"} 3
|
||||
telemt_tls_front_profile_ticket_records{domain="itunes.apple.com"} 1
|
||||
telemt_tls_front_profile_change_cipher_spec_records{domain="itunes.apple.com"} 1
|
||||
telemt_tls_front_profile_app_data_bytes{domain="itunes.apple.com"} 5240
|
||||
```
|
||||
|
||||
Эти метрики не доказывают побайтную эквивалентность с origin. Это эксплуатационный сигнал состояния: настроенный домен действительно основан на реальных закешированных данных профиля, а не на default fallback.
|
||||
|
||||
### 5. Снять direct-origin trace
|
||||
|
||||
С отдельной клиентской машины подключитесь напрямую к origin:
|
||||
|
||||
@@ -142,7 +183,7 @@ Capture:
|
||||
sudo tcpdump -i any -w origin-direct.pcap host ORIGIN_IP and port 443
|
||||
```
|
||||
|
||||
### 5. Снять Telemt FakeTLS success-path trace
|
||||
### 6. Снять Telemt FakeTLS success-path trace
|
||||
|
||||
Теперь подключитесь к Telemt через реальный Telegram client с `ee` proxy link, который указывает на Telemt instance.
|
||||
|
||||
@@ -154,7 +195,7 @@ Capture:
|
||||
sudo tcpdump -i any -w telemt-emulated.pcap host TELEMT_IP and port 443
|
||||
```
|
||||
|
||||
### 6. Декодировать структуру TLS records
|
||||
### 7. Декодировать структуру TLS records
|
||||
|
||||
Используйте `tshark`, чтобы вывести record-level structure:
|
||||
|
||||
@@ -182,7 +223,7 @@ tshark -r telemt-emulated.pcap -Y "tls.record" -T fields \
|
||||
- `20` = ChangeCipherSpec
|
||||
- `23` = ApplicationData
|
||||
|
||||
### 7. Собрать сравнительную таблицу
|
||||
### 8. Собрать сравнительную таблицу
|
||||
|
||||
Обычно достаточно короткой таблицы такого вида:
|
||||
|
||||
|
||||
@@ -23,6 +23,666 @@ const MAX_ME_C2ME_CHANNEL_CAPACITY: usize = 8_192;
|
||||
const MIN_MAX_CLIENT_FRAME_BYTES: usize = 4 * 1024;
|
||||
const MAX_MAX_CLIENT_FRAME_BYTES: usize = 16 * 1024 * 1024;
|
||||
|
||||
const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
|
||||
"general",
|
||||
"network",
|
||||
"server",
|
||||
"timeouts",
|
||||
"censorship",
|
||||
"access",
|
||||
"upstreams",
|
||||
"show_link",
|
||||
"dc_overrides",
|
||||
"default_dc",
|
||||
"beobachten",
|
||||
"beobachten_minutes",
|
||||
"beobachten_flush_secs",
|
||||
"beobachten_file",
|
||||
"include",
|
||||
];
|
||||
|
||||
const GENERAL_CONFIG_KEYS: &[&str] = &[
|
||||
"data_path",
|
||||
"config_strict",
|
||||
"modes",
|
||||
"prefer_ipv6",
|
||||
"fast_mode",
|
||||
"use_middle_proxy",
|
||||
"proxy_secret_path",
|
||||
"proxy_secret_url",
|
||||
"proxy_config_v4_cache_path",
|
||||
"proxy_config_v4_url",
|
||||
"proxy_config_v6_cache_path",
|
||||
"proxy_config_v6_url",
|
||||
"ad_tag",
|
||||
"middle_proxy_nat_ip",
|
||||
"middle_proxy_nat_probe",
|
||||
"middle_proxy_nat_stun",
|
||||
"middle_proxy_nat_stun_servers",
|
||||
"stun_nat_probe_concurrency",
|
||||
"middle_proxy_pool_size",
|
||||
"middle_proxy_warm_standby",
|
||||
"me_init_retry_attempts",
|
||||
"me2dc_fallback",
|
||||
"me2dc_fast",
|
||||
"me_keepalive_enabled",
|
||||
"me_keepalive_interval_secs",
|
||||
"me_keepalive_jitter_secs",
|
||||
"me_keepalive_payload_random",
|
||||
"rpc_proxy_req_every",
|
||||
"me_writer_cmd_channel_capacity",
|
||||
"me_route_channel_capacity",
|
||||
"me_c2me_channel_capacity",
|
||||
"me_c2me_send_timeout_ms",
|
||||
"me_reader_route_data_wait_ms",
|
||||
"me_d2c_flush_batch_max_frames",
|
||||
"me_d2c_flush_batch_max_bytes",
|
||||
"me_d2c_flush_batch_max_delay_us",
|
||||
"me_d2c_ack_flush_immediate",
|
||||
"me_quota_soft_overshoot_bytes",
|
||||
"me_d2c_frame_buf_shrink_threshold_bytes",
|
||||
"direct_relay_copy_buf_c2s_bytes",
|
||||
"direct_relay_copy_buf_s2c_bytes",
|
||||
"crypto_pending_buffer",
|
||||
"max_client_frame",
|
||||
"desync_all_full",
|
||||
"beobachten",
|
||||
"beobachten_minutes",
|
||||
"beobachten_flush_secs",
|
||||
"beobachten_file",
|
||||
"hardswap",
|
||||
"me_warmup_stagger_enabled",
|
||||
"me_warmup_step_delay_ms",
|
||||
"me_warmup_step_jitter_ms",
|
||||
"me_reconnect_max_concurrent_per_dc",
|
||||
"me_reconnect_backoff_base_ms",
|
||||
"me_reconnect_backoff_cap_ms",
|
||||
"me_reconnect_fast_retry_count",
|
||||
"me_single_endpoint_shadow_writers",
|
||||
"me_single_endpoint_outage_mode_enabled",
|
||||
"me_single_endpoint_outage_disable_quarantine",
|
||||
"me_single_endpoint_outage_backoff_min_ms",
|
||||
"me_single_endpoint_outage_backoff_max_ms",
|
||||
"me_single_endpoint_shadow_rotate_every_secs",
|
||||
"me_floor_mode",
|
||||
"me_adaptive_floor_idle_secs",
|
||||
"me_adaptive_floor_min_writers_single_endpoint",
|
||||
"me_adaptive_floor_min_writers_multi_endpoint",
|
||||
"me_adaptive_floor_recover_grace_secs",
|
||||
"me_adaptive_floor_writers_per_core_total",
|
||||
"me_adaptive_floor_cpu_cores_override",
|
||||
"me_adaptive_floor_max_extra_writers_single_per_core",
|
||||
"me_adaptive_floor_max_extra_writers_multi_per_core",
|
||||
"me_adaptive_floor_max_active_writers_per_core",
|
||||
"me_adaptive_floor_max_warm_writers_per_core",
|
||||
"me_adaptive_floor_max_active_writers_global",
|
||||
"me_adaptive_floor_max_warm_writers_global",
|
||||
"upstream_connect_retry_attempts",
|
||||
"upstream_connect_retry_backoff_ms",
|
||||
"upstream_connect_budget_ms",
|
||||
"tg_connect",
|
||||
"upstream_unhealthy_fail_threshold",
|
||||
"upstream_connect_failfast_hard_errors",
|
||||
"stun_iface_mismatch_ignore",
|
||||
"unknown_dc_log_path",
|
||||
"unknown_dc_file_log_enabled",
|
||||
"log_level",
|
||||
"disable_colors",
|
||||
"telemetry",
|
||||
"me_socks_kdf_policy",
|
||||
"me_route_backpressure_enabled",
|
||||
"me_route_fairshare_enabled",
|
||||
"me_route_backpressure_base_timeout_ms",
|
||||
"me_route_backpressure_high_timeout_ms",
|
||||
"me_route_backpressure_high_watermark_pct",
|
||||
"me_health_interval_ms_unhealthy",
|
||||
"me_health_interval_ms_healthy",
|
||||
"me_admission_poll_ms",
|
||||
"me_warn_rate_limit_ms",
|
||||
"me_route_no_writer_mode",
|
||||
"me_route_no_writer_wait_ms",
|
||||
"me_route_hybrid_max_wait_ms",
|
||||
"me_route_blocking_send_timeout_ms",
|
||||
"me_route_inline_recovery_attempts",
|
||||
"me_route_inline_recovery_wait_ms",
|
||||
"links",
|
||||
"fast_mode_min_tls_record",
|
||||
"update_every",
|
||||
"me_reinit_every_secs",
|
||||
"me_hardswap_warmup_delay_min_ms",
|
||||
"me_hardswap_warmup_delay_max_ms",
|
||||
"me_hardswap_warmup_extra_passes",
|
||||
"me_hardswap_warmup_pass_backoff_base_ms",
|
||||
"me_config_stable_snapshots",
|
||||
"me_config_apply_cooldown_secs",
|
||||
"me_snapshot_require_http_2xx",
|
||||
"me_snapshot_reject_empty_map",
|
||||
"me_snapshot_min_proxy_for_lines",
|
||||
"proxy_secret_stable_snapshots",
|
||||
"proxy_secret_rotate_runtime",
|
||||
"me_secret_atomic_snapshot",
|
||||
"proxy_secret_len_max",
|
||||
"me_pool_drain_ttl_secs",
|
||||
"me_instadrain",
|
||||
"me_pool_drain_threshold",
|
||||
"me_pool_drain_soft_evict_enabled",
|
||||
"me_pool_drain_soft_evict_grace_secs",
|
||||
"me_pool_drain_soft_evict_per_writer",
|
||||
"me_pool_drain_soft_evict_budget_per_core",
|
||||
"me_pool_drain_soft_evict_cooldown_ms",
|
||||
"me_bind_stale_mode",
|
||||
"me_bind_stale_ttl_secs",
|
||||
"me_pool_min_fresh_ratio",
|
||||
"me_reinit_drain_timeout_secs",
|
||||
"proxy_secret_auto_reload_secs",
|
||||
"proxy_config_auto_reload_secs",
|
||||
"me_reinit_singleflight",
|
||||
"me_reinit_trigger_channel",
|
||||
"me_reinit_coalesce_window_ms",
|
||||
"me_deterministic_writer_sort",
|
||||
"me_writer_pick_mode",
|
||||
"me_writer_pick_sample_size",
|
||||
"ntp_check",
|
||||
"ntp_servers",
|
||||
"auto_degradation_enabled",
|
||||
"degradation_min_unavailable_dc_groups",
|
||||
"rst_on_close",
|
||||
];
|
||||
|
||||
const NETWORK_CONFIG_KEYS: &[&str] = &[
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
"prefer",
|
||||
"multipath",
|
||||
"stun_use",
|
||||
"stun_servers",
|
||||
"stun_tcp_fallback",
|
||||
"http_ip_detect_urls",
|
||||
"cache_public_ip_path",
|
||||
"dns_overrides",
|
||||
];
|
||||
|
||||
const SERVER_CONFIG_KEYS: &[&str] = &[
|
||||
"port",
|
||||
"listen_addr_ipv4",
|
||||
"listen_addr_ipv6",
|
||||
"listen_unix_sock",
|
||||
"listen_unix_sock_perm",
|
||||
"listen_tcp",
|
||||
"proxy_protocol",
|
||||
"proxy_protocol_header_timeout_ms",
|
||||
"proxy_protocol_trusted_cidrs",
|
||||
"metrics_port",
|
||||
"metrics_listen",
|
||||
"metrics_whitelist",
|
||||
"api",
|
||||
"admin_api",
|
||||
"listeners",
|
||||
"listen_backlog",
|
||||
"max_connections",
|
||||
"accept_permit_timeout_ms",
|
||||
"conntrack_control",
|
||||
];
|
||||
|
||||
const API_CONFIG_KEYS: &[&str] = &[
|
||||
"enabled",
|
||||
"listen",
|
||||
"whitelist",
|
||||
"gray_action",
|
||||
"auth_header",
|
||||
"request_body_limit_bytes",
|
||||
"minimal_runtime_enabled",
|
||||
"minimal_runtime_cache_ttl_ms",
|
||||
"runtime_edge_enabled",
|
||||
"runtime_edge_cache_ttl_ms",
|
||||
"runtime_edge_top_n",
|
||||
"runtime_edge_events_capacity",
|
||||
"read_only",
|
||||
];
|
||||
|
||||
const CONNTRACK_CONTROL_CONFIG_KEYS: &[&str] = &[
|
||||
"inline_conntrack_control",
|
||||
"mode",
|
||||
"backend",
|
||||
"profile",
|
||||
"hybrid_listener_ips",
|
||||
"pressure_high_watermark_pct",
|
||||
"pressure_low_watermark_pct",
|
||||
"delete_budget_per_sec",
|
||||
];
|
||||
|
||||
const LISTENER_CONFIG_KEYS: &[&str] = &[
|
||||
"ip",
|
||||
"port",
|
||||
"announce",
|
||||
"announce_ip",
|
||||
"proxy_protocol",
|
||||
"reuse_allow",
|
||||
];
|
||||
|
||||
const TIMEOUTS_CONFIG_KEYS: &[&str] = &[
|
||||
"client_first_byte_idle_secs",
|
||||
"client_handshake",
|
||||
"relay_idle_policy_v2_enabled",
|
||||
"relay_client_idle_soft_secs",
|
||||
"relay_client_idle_hard_secs",
|
||||
"relay_idle_grace_after_downstream_activity_secs",
|
||||
"client_keepalive",
|
||||
"client_ack",
|
||||
"me_one_retry",
|
||||
"me_one_timeout_ms",
|
||||
];
|
||||
|
||||
const CENSORSHIP_CONFIG_KEYS: &[&str] = &[
|
||||
"tls_domain",
|
||||
"tls_domains",
|
||||
"unknown_sni_action",
|
||||
"tls_fetch_scope",
|
||||
"tls_fetch",
|
||||
"mask",
|
||||
"mask_host",
|
||||
"mask_port",
|
||||
"mask_unix_sock",
|
||||
"fake_cert_len",
|
||||
"tls_emulation",
|
||||
"tls_front_dir",
|
||||
"server_hello_delay_min_ms",
|
||||
"server_hello_delay_max_ms",
|
||||
"tls_new_session_tickets",
|
||||
"serverhello_compact",
|
||||
"tls_full_cert_ttl_secs",
|
||||
"alpn_enforce",
|
||||
"mask_proxy_protocol",
|
||||
"mask_shape_hardening",
|
||||
"mask_shape_hardening_aggressive_mode",
|
||||
"mask_shape_bucket_floor_bytes",
|
||||
"mask_shape_bucket_cap_bytes",
|
||||
"mask_shape_above_cap_blur",
|
||||
"mask_shape_above_cap_blur_max_bytes",
|
||||
"mask_relay_max_bytes",
|
||||
"mask_relay_timeout_ms",
|
||||
"mask_relay_idle_timeout_ms",
|
||||
"mask_classifier_prefetch_timeout_ms",
|
||||
"mask_timing_normalization_enabled",
|
||||
"mask_timing_normalization_floor_ms",
|
||||
"mask_timing_normalization_ceiling_ms",
|
||||
];
|
||||
|
||||
const TLS_FETCH_CONFIG_KEYS: &[&str] = &[
|
||||
"profiles",
|
||||
"strict_route",
|
||||
"attempt_timeout_ms",
|
||||
"total_budget_ms",
|
||||
"grease_enabled",
|
||||
"deterministic",
|
||||
"profile_cache_ttl_secs",
|
||||
];
|
||||
|
||||
const ACCESS_CONFIG_KEYS: &[&str] = &[
|
||||
"users",
|
||||
"user_ad_tags",
|
||||
"user_max_tcp_conns",
|
||||
"user_max_tcp_conns_global_each",
|
||||
"user_expirations",
|
||||
"user_data_quota",
|
||||
"user_rate_limits",
|
||||
"cidr_rate_limits",
|
||||
"user_max_unique_ips",
|
||||
"user_max_unique_ips_global_each",
|
||||
"user_max_unique_ips_mode",
|
||||
"user_max_unique_ips_window_secs",
|
||||
"replay_check_len",
|
||||
"replay_window_secs",
|
||||
"ignore_time_skew",
|
||||
];
|
||||
|
||||
const RATE_LIMIT_BPS_CONFIG_KEYS: &[&str] = &["up_bps", "down_bps"];
|
||||
|
||||
const UPSTREAM_CONFIG_KEYS: &[&str] = &[
|
||||
"type",
|
||||
"interface",
|
||||
"bind_addresses",
|
||||
"bindtodevice",
|
||||
"force_bind",
|
||||
"address",
|
||||
"user_id",
|
||||
"username",
|
||||
"password",
|
||||
"url",
|
||||
"weight",
|
||||
"enabled",
|
||||
"scopes",
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
];
|
||||
|
||||
const PROXY_MODES_CONFIG_KEYS: &[&str] = &["classic", "secure", "tls"];
|
||||
const TELEMETRY_CONFIG_KEYS: &[&str] = &["core_enabled", "user_enabled", "me_level"];
|
||||
const LINKS_CONFIG_KEYS: &[&str] = &["show", "public_host", "public_port"];
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UnknownConfigKey {
|
||||
path: String,
|
||||
suggestion: Option<String>,
|
||||
}
|
||||
|
||||
fn table_at<'a>(value: &'a toml::Value, path: &[&str]) -> Option<&'a toml::Table> {
|
||||
let mut current = value;
|
||||
for segment in path {
|
||||
current = current.get(*segment)?;
|
||||
}
|
||||
current.as_table()
|
||||
}
|
||||
|
||||
fn is_strict_config(parsed_toml: &toml::Value) -> bool {
|
||||
table_at(parsed_toml, &["general"])
|
||||
.and_then(|table| table.get("config_strict"))
|
||||
.and_then(toml::Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn known_config_keys_for_suggestion() -> Vec<&'static str> {
|
||||
let mut keys = Vec::new();
|
||||
for group in [
|
||||
TOP_LEVEL_CONFIG_KEYS,
|
||||
GENERAL_CONFIG_KEYS,
|
||||
NETWORK_CONFIG_KEYS,
|
||||
SERVER_CONFIG_KEYS,
|
||||
API_CONFIG_KEYS,
|
||||
CONNTRACK_CONTROL_CONFIG_KEYS,
|
||||
LISTENER_CONFIG_KEYS,
|
||||
TIMEOUTS_CONFIG_KEYS,
|
||||
CENSORSHIP_CONFIG_KEYS,
|
||||
TLS_FETCH_CONFIG_KEYS,
|
||||
ACCESS_CONFIG_KEYS,
|
||||
RATE_LIMIT_BPS_CONFIG_KEYS,
|
||||
UPSTREAM_CONFIG_KEYS,
|
||||
PROXY_MODES_CONFIG_KEYS,
|
||||
TELEMETRY_CONFIG_KEYS,
|
||||
LINKS_CONFIG_KEYS,
|
||||
] {
|
||||
keys.extend_from_slice(group);
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn levenshtein_distance(a: &str, b: &str) -> usize {
|
||||
let b_chars: Vec<char> = b.chars().collect();
|
||||
let mut prev: Vec<usize> = (0..=b_chars.len()).collect();
|
||||
let mut curr = vec![0usize; b_chars.len() + 1];
|
||||
|
||||
for (i, ca) in a.chars().enumerate() {
|
||||
curr[0] = i + 1;
|
||||
for (j, cb) in b_chars.iter().enumerate() {
|
||||
let replace = if ca == *cb { prev[j] } else { prev[j] + 1 };
|
||||
curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(replace);
|
||||
}
|
||||
std::mem::swap(&mut prev, &mut curr);
|
||||
}
|
||||
|
||||
prev[b_chars.len()]
|
||||
}
|
||||
|
||||
fn unknown_key_suggestion(key: &str, known_keys: &[&'static str]) -> Option<String> {
|
||||
let normalized = key.to_ascii_lowercase();
|
||||
let mut best: Option<(&str, usize)> = None;
|
||||
for known in known_keys {
|
||||
let distance = levenshtein_distance(&normalized, known);
|
||||
let is_better = match best {
|
||||
Some((_, best_distance)) => distance < best_distance,
|
||||
None => true,
|
||||
};
|
||||
if distance <= 4 && is_better {
|
||||
best = Some((known, distance));
|
||||
}
|
||||
}
|
||||
best.map(|(known, _)| known.to_string())
|
||||
}
|
||||
|
||||
fn push_unknown_keys(
|
||||
unknown: &mut Vec<UnknownConfigKey>,
|
||||
known_for_suggestion: &[&'static str],
|
||||
path: &str,
|
||||
table: &toml::Table,
|
||||
allowed: &[&str],
|
||||
) {
|
||||
for key in table.keys() {
|
||||
if !allowed.contains(&key.as_str()) {
|
||||
let full_path = if path.is_empty() {
|
||||
key.clone()
|
||||
} else {
|
||||
format!("{path}.{key}")
|
||||
};
|
||||
unknown.push(UnknownConfigKey {
|
||||
path: full_path,
|
||||
suggestion: unknown_key_suggestion(key, known_for_suggestion),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_known_table(
|
||||
parsed_toml: &toml::Value,
|
||||
unknown: &mut Vec<UnknownConfigKey>,
|
||||
known_for_suggestion: &[&'static str],
|
||||
path: &[&str],
|
||||
allowed: &[&str],
|
||||
) {
|
||||
if let Some(table) = table_at(parsed_toml, path) {
|
||||
push_unknown_keys(
|
||||
unknown,
|
||||
known_for_suggestion,
|
||||
&path.join("."),
|
||||
table,
|
||||
allowed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_nested_table_value(
|
||||
unknown: &mut Vec<UnknownConfigKey>,
|
||||
known_for_suggestion: &[&'static str],
|
||||
path: String,
|
||||
value: &toml::Value,
|
||||
allowed: &[&str],
|
||||
) {
|
||||
if let Some(table) = value.as_table() {
|
||||
push_unknown_keys(unknown, known_for_suggestion, &path, table, allowed);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_unknown_config_keys(parsed_toml: &toml::Value) -> Vec<UnknownConfigKey> {
|
||||
let known_for_suggestion = known_config_keys_for_suggestion();
|
||||
let mut unknown = Vec::new();
|
||||
|
||||
if let Some(root) = parsed_toml.as_table() {
|
||||
push_unknown_keys(
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
"",
|
||||
root,
|
||||
TOP_LEVEL_CONFIG_KEYS,
|
||||
);
|
||||
}
|
||||
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["general"],
|
||||
GENERAL_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["general", "modes"],
|
||||
PROXY_MODES_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["general", "telemetry"],
|
||||
TELEMETRY_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["general", "links"],
|
||||
LINKS_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["network"],
|
||||
NETWORK_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["server"],
|
||||
SERVER_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["server", "api"],
|
||||
API_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["server", "admin_api"],
|
||||
API_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["server", "conntrack_control"],
|
||||
CONNTRACK_CONTROL_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["timeouts"],
|
||||
TIMEOUTS_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["censorship"],
|
||||
CENSORSHIP_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["censorship", "tls_fetch"],
|
||||
TLS_FETCH_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["access"],
|
||||
ACCESS_CONFIG_KEYS,
|
||||
);
|
||||
|
||||
if let Some(listeners) = table_at(parsed_toml, &["server"])
|
||||
.and_then(|table| table.get("listeners"))
|
||||
.and_then(toml::Value::as_array)
|
||||
{
|
||||
for (idx, listener) in listeners.iter().enumerate() {
|
||||
check_nested_table_value(
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
format!("server.listeners[{idx}]"),
|
||||
listener,
|
||||
LISTENER_CONFIG_KEYS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(upstreams) = parsed_toml
|
||||
.get("upstreams")
|
||||
.and_then(toml::Value::as_array)
|
||||
{
|
||||
for (idx, upstream) in upstreams.iter().enumerate() {
|
||||
check_nested_table_value(
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
format!("upstreams[{idx}]"),
|
||||
upstream,
|
||||
UPSTREAM_CONFIG_KEYS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for access_map in ["user_rate_limits", "cidr_rate_limits"] {
|
||||
if let Some(table) = table_at(parsed_toml, &["access"])
|
||||
.and_then(|access| access.get(access_map))
|
||||
.and_then(toml::Value::as_table)
|
||||
{
|
||||
for (entry_name, value) in table {
|
||||
check_nested_table_value(
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
format!("access.{access_map}.{entry_name}"),
|
||||
value,
|
||||
RATE_LIMIT_BPS_CONFIG_KEYS,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unknown
|
||||
}
|
||||
|
||||
fn handle_unknown_config_keys(parsed_toml: &toml::Value) -> Result<()> {
|
||||
let unknown = collect_unknown_config_keys(parsed_toml);
|
||||
if unknown.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for item in &unknown {
|
||||
if let Some(suggestion) = item.suggestion.as_deref() {
|
||||
warn!(
|
||||
key = %item.path,
|
||||
suggestion = %suggestion,
|
||||
"Unknown config key ignored; did you mean the suggested key?"
|
||||
);
|
||||
} else {
|
||||
warn!(key = %item.path, "Unknown config key ignored");
|
||||
}
|
||||
}
|
||||
|
||||
if is_strict_config(parsed_toml) {
|
||||
let mut paths = Vec::with_capacity(unknown.len());
|
||||
for item in unknown {
|
||||
if let Some(suggestion) = item.suggestion {
|
||||
paths.push(format!("{} (did you mean `{}`?)", item.path, suggestion));
|
||||
} else {
|
||||
paths.push(item.path);
|
||||
}
|
||||
}
|
||||
return Err(ProxyError::Config(format!(
|
||||
"unknown config keys are not allowed when general.config_strict=true: {}",
|
||||
paths.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LoadedConfig {
|
||||
pub(crate) config: ProxyConfig,
|
||||
@@ -337,6 +997,7 @@ impl ProxyConfig {
|
||||
|
||||
let parsed_toml: toml::Value =
|
||||
toml::from_str(&processed).map_err(|e| ProxyError::Config(e.to_string()))?;
|
||||
handle_unknown_config_keys(&parsed_toml)?;
|
||||
let general_table = parsed_toml
|
||||
.get("general")
|
||||
.and_then(|value| value.as_table());
|
||||
|
||||
@@ -375,6 +375,11 @@ pub struct GeneralConfig {
|
||||
#[serde(default)]
|
||||
pub data_path: Option<PathBuf>,
|
||||
|
||||
/// Reject unknown TOML config keys during load.
|
||||
/// Startup fails fast; hot-reload rejects the new snapshot and keeps the current config.
|
||||
#[serde(default)]
|
||||
pub config_strict: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub modes: ProxyModes,
|
||||
|
||||
@@ -974,6 +979,7 @@ impl Default for GeneralConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data_path: None,
|
||||
config_strict: false,
|
||||
modes: ProxyModes::default(),
|
||||
prefer_ipv6: false,
|
||||
fast_mode: default_true(),
|
||||
|
||||
@@ -814,6 +814,7 @@ async fn run_telemt_core(
|
||||
beobachten.clone(),
|
||||
shared_state.clone(),
|
||||
ip_tracker.clone(),
|
||||
tls_cache.clone(),
|
||||
config_rx.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::startup::{
|
||||
use crate::stats::beobachten::BeobachtenStore;
|
||||
use crate::stats::telemetry::TelemetryPolicy;
|
||||
use crate::stats::{ReplayChecker, Stats};
|
||||
use crate::tls_front::TlsFrontCache;
|
||||
use crate::transport::UpstreamManager;
|
||||
use crate::transport::middle_proxy::{MePool, MeReinitTrigger};
|
||||
|
||||
@@ -328,6 +329,7 @@ pub(crate) async fn spawn_metrics_if_configured(
|
||||
beobachten: Arc<BeobachtenStore>,
|
||||
shared_state: Arc<ProxySharedState>,
|
||||
ip_tracker: Arc<UserIpTracker>,
|
||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||
) {
|
||||
// metrics_listen takes precedence; fall back to metrics_port for backward compat.
|
||||
@@ -363,6 +365,7 @@ pub(crate) async fn spawn_metrics_if_configured(
|
||||
let shared_state = shared_state.clone();
|
||||
let config_rx_metrics = config_rx.clone();
|
||||
let ip_tracker_metrics = ip_tracker.clone();
|
||||
let tls_cache_metrics = tls_cache.clone();
|
||||
let whitelist = config.server.metrics_whitelist.clone();
|
||||
let listen_backlog = config.server.listen_backlog;
|
||||
tokio::spawn(async move {
|
||||
@@ -374,6 +377,7 @@ pub(crate) async fn spawn_metrics_if_configured(
|
||||
beobachten,
|
||||
shared_state,
|
||||
ip_tracker_metrics,
|
||||
tls_cache_metrics,
|
||||
config_rx_metrics,
|
||||
whitelist,
|
||||
)
|
||||
|
||||
296
src/metrics.rs
296
src/metrics.rs
@@ -18,12 +18,15 @@ use crate::ip_tracker::UserIpTracker;
|
||||
use crate::proxy::shared_state::ProxySharedState;
|
||||
use crate::stats::Stats;
|
||||
use crate::stats::beobachten::BeobachtenStore;
|
||||
use crate::tls_front::TlsFrontCache;
|
||||
use crate::tls_front::cache;
|
||||
use crate::tls_front::fetcher;
|
||||
use crate::transport::{ListenOptions, create_listener};
|
||||
|
||||
// Keeps `/metrics` response size bounded when per-user telemetry is enabled.
|
||||
const USER_LABELED_METRICS_MAX_USERS: usize = 4096;
|
||||
// Keeps TLS-front per-domain health series bounded for large generated configs.
|
||||
const TLS_FRONT_PROFILE_HEALTH_MAX_DOMAINS: usize = 256;
|
||||
|
||||
pub async fn serve(
|
||||
port: u16,
|
||||
@@ -33,6 +36,7 @@ pub async fn serve(
|
||||
beobachten: Arc<BeobachtenStore>,
|
||||
shared_state: Arc<ProxySharedState>,
|
||||
ip_tracker: Arc<UserIpTracker>,
|
||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||
config_rx: tokio::sync::watch::Receiver<Arc<ProxyConfig>>,
|
||||
whitelist: Vec<IpNetwork>,
|
||||
) {
|
||||
@@ -57,6 +61,7 @@ pub async fn serve(
|
||||
beobachten,
|
||||
shared_state,
|
||||
ip_tracker,
|
||||
tls_cache,
|
||||
config_rx,
|
||||
whitelist,
|
||||
)
|
||||
@@ -112,6 +117,7 @@ pub async fn serve(
|
||||
beobachten,
|
||||
shared_state,
|
||||
ip_tracker,
|
||||
tls_cache,
|
||||
config_rx,
|
||||
whitelist,
|
||||
)
|
||||
@@ -122,6 +128,7 @@ pub async fn serve(
|
||||
let beobachten_v6 = beobachten.clone();
|
||||
let shared_state_v6 = shared_state.clone();
|
||||
let ip_tracker_v6 = ip_tracker.clone();
|
||||
let tls_cache_v6 = tls_cache.clone();
|
||||
let config_rx_v6 = config_rx.clone();
|
||||
let whitelist_v6 = whitelist.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -131,6 +138,7 @@ pub async fn serve(
|
||||
beobachten_v6,
|
||||
shared_state_v6,
|
||||
ip_tracker_v6,
|
||||
tls_cache_v6,
|
||||
config_rx_v6,
|
||||
whitelist_v6,
|
||||
)
|
||||
@@ -142,6 +150,7 @@ pub async fn serve(
|
||||
beobachten,
|
||||
shared_state,
|
||||
ip_tracker,
|
||||
tls_cache,
|
||||
config_rx,
|
||||
whitelist,
|
||||
)
|
||||
@@ -171,6 +180,7 @@ async fn serve_listener(
|
||||
beobachten: Arc<BeobachtenStore>,
|
||||
shared_state: Arc<ProxySharedState>,
|
||||
ip_tracker: Arc<UserIpTracker>,
|
||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||
config_rx: tokio::sync::watch::Receiver<Arc<ProxyConfig>>,
|
||||
whitelist: Arc<Vec<IpNetwork>>,
|
||||
) {
|
||||
@@ -192,6 +202,7 @@ async fn serve_listener(
|
||||
let beobachten = beobachten.clone();
|
||||
let shared_state = shared_state.clone();
|
||||
let ip_tracker = ip_tracker.clone();
|
||||
let tls_cache = tls_cache.clone();
|
||||
let config_rx_conn = config_rx.clone();
|
||||
tokio::spawn(async move {
|
||||
let svc = service_fn(move |req| {
|
||||
@@ -199,6 +210,7 @@ async fn serve_listener(
|
||||
let beobachten = beobachten.clone();
|
||||
let shared_state = shared_state.clone();
|
||||
let ip_tracker = ip_tracker.clone();
|
||||
let tls_cache = tls_cache.clone();
|
||||
let config = config_rx_conn.borrow().clone();
|
||||
async move {
|
||||
handle(
|
||||
@@ -207,6 +219,7 @@ async fn serve_listener(
|
||||
&beobachten,
|
||||
&shared_state,
|
||||
&ip_tracker,
|
||||
tls_cache.as_deref(),
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
@@ -228,10 +241,11 @@ async fn handle<B>(
|
||||
beobachten: &BeobachtenStore,
|
||||
shared_state: &ProxySharedState,
|
||||
ip_tracker: &UserIpTracker,
|
||||
tls_cache: Option<&TlsFrontCache>,
|
||||
config: &ProxyConfig,
|
||||
) -> Result<Response<Full<Bytes>>, Infallible> {
|
||||
if req.uri().path() == "/metrics" {
|
||||
let body = render_metrics(stats, shared_state, config, ip_tracker).await;
|
||||
let body = render_metrics(stats, shared_state, config, ip_tracker, tls_cache).await;
|
||||
let resp = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("content-type", "text/plain; version=0.0.4; charset=utf-8")
|
||||
@@ -266,11 +280,151 @@ fn render_beobachten(beobachten: &BeobachtenStore, config: &ProxyConfig) -> Stri
|
||||
beobachten.snapshot_text(ttl)
|
||||
}
|
||||
|
||||
fn tls_front_domains(config: &ProxyConfig) -> Vec<String> {
|
||||
let mut domains = Vec::with_capacity(1 + config.censorship.tls_domains.len());
|
||||
if !config.censorship.tls_domain.is_empty() {
|
||||
domains.push(config.censorship.tls_domain.clone());
|
||||
}
|
||||
for domain in &config.censorship.tls_domains {
|
||||
if !domain.is_empty() && !domains.contains(domain) {
|
||||
domains.push(domain.clone());
|
||||
}
|
||||
}
|
||||
domains
|
||||
}
|
||||
|
||||
fn prometheus_label_value(value: &str) -> String {
|
||||
value.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
}
|
||||
|
||||
async fn render_tls_front_profile_health(
|
||||
out: &mut String,
|
||||
config: &ProxyConfig,
|
||||
tls_cache: Option<&TlsFrontCache>,
|
||||
) {
|
||||
use std::fmt::Write;
|
||||
|
||||
let domains = tls_front_domains(config);
|
||||
let (health, suppressed) = match (config.censorship.tls_emulation, tls_cache) {
|
||||
(true, Some(cache)) => {
|
||||
cache
|
||||
.profile_health_snapshot(&domains, TLS_FRONT_PROFILE_HEALTH_MAX_DOMAINS)
|
||||
.await
|
||||
}
|
||||
_ => (Vec::new(), domains.len()),
|
||||
};
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_domains TLS front configured profile domains by export status"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_domains gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_domains{{status=\"configured\"}} {}",
|
||||
domains.len()
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_domains{{status=\"emitted\"}} {}",
|
||||
health.len()
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_domains{{status=\"suppressed\"}} {}",
|
||||
suppressed
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_info TLS front profile source and feature flags per configured domain"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_info gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_age_seconds Age of cached TLS front profile data per configured domain"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# TYPE telemt_tls_front_profile_age_seconds gauge"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_app_data_records TLS front cached app-data record count per configured domain"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# TYPE telemt_tls_front_profile_app_data_records gauge"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_ticket_records TLS front cached ticket-like tail record count per configured domain"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# TYPE telemt_tls_front_profile_ticket_records gauge"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_change_cipher_spec_records TLS front cached ChangeCipherSpec record count per configured domain"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# TYPE telemt_tls_front_profile_change_cipher_spec_records gauge"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_app_data_bytes TLS front cached total app-data bytes per configured domain"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# TYPE telemt_tls_front_profile_app_data_bytes gauge"
|
||||
);
|
||||
|
||||
for item in health {
|
||||
let domain = prometheus_label_value(&item.domain);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_info{{domain=\"{}\",source=\"{}\",is_default=\"{}\",has_cert_info=\"{}\",has_cert_payload=\"{}\"}} 1",
|
||||
domain,
|
||||
item.source,
|
||||
item.is_default,
|
||||
item.has_cert_info,
|
||||
item.has_cert_payload
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_age_seconds{{domain=\"{}\"}} {}",
|
||||
domain, item.age_seconds
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_app_data_records{{domain=\"{}\"}} {}",
|
||||
domain, item.app_data_records
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_ticket_records{{domain=\"{}\"}} {}",
|
||||
domain, item.ticket_records
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_change_cipher_spec_records{{domain=\"{}\"}} {}",
|
||||
domain, item.change_cipher_spec_count
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_app_data_bytes{{domain=\"{}\"}} {}",
|
||||
domain, item.total_app_data_len
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn render_metrics(
|
||||
stats: &Stats,
|
||||
shared_state: &ProxySharedState,
|
||||
config: &ProxyConfig,
|
||||
ip_tracker: &UserIpTracker,
|
||||
tls_cache: Option<&TlsFrontCache>,
|
||||
) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut out = String::with_capacity(4096);
|
||||
@@ -423,6 +577,7 @@ async fn render_metrics(
|
||||
"telemt_tls_front_full_cert_budget_cap_drops_total {}",
|
||||
cache::full_cert_sent_cap_drops_for_metrics()
|
||||
);
|
||||
render_tls_front_profile_health(&mut out, config, tls_cache).await;
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
@@ -454,6 +609,21 @@ async fn render_metrics(
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_connections_bad_by_class_total Bad/rejected connections by class"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_connections_bad_by_class_total counter");
|
||||
if core_enabled {
|
||||
for (class, total) in stats.get_connects_bad_class_counts() {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_connections_bad_by_class_total{{class=\"{}\"}} {}",
|
||||
class, total
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_handshake_timeouts_total Handshake timeouts"
|
||||
@@ -469,6 +639,24 @@ async fn render_metrics(
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_handshake_failures_by_class_total Handshake failures by class"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# TYPE telemt_handshake_failures_by_class_total counter"
|
||||
);
|
||||
if core_enabled {
|
||||
for (class, total) in stats.get_handshake_failure_class_counts() {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_handshake_failures_by_class_total{{class=\"{}\"}} {}",
|
||||
class, total
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_auth_expensive_checks_total Expensive authentication candidate checks executed during handshake validation"
|
||||
@@ -3328,6 +3516,11 @@ mod tests {
|
||||
use super::*;
|
||||
use http_body_util::BodyExt;
|
||||
use std::net::IpAddr;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsProfileSource,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_render_metrics_format() {
|
||||
@@ -3342,8 +3535,9 @@ mod tests {
|
||||
|
||||
stats.increment_connects_all();
|
||||
stats.increment_connects_all();
|
||||
stats.increment_connects_bad();
|
||||
stats.increment_connects_bad_with_class("tls_handshake_bad_client");
|
||||
stats.increment_handshake_timeouts();
|
||||
stats.increment_handshake_failure_class("timeout");
|
||||
shared_state
|
||||
.handshake
|
||||
.auth_expensive_checks_total
|
||||
@@ -3395,7 +3589,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let output = render_metrics(&stats, shared_state.as_ref(), &config, &tracker).await;
|
||||
let output = render_metrics(&stats, shared_state.as_ref(), &config, &tracker, None).await;
|
||||
|
||||
assert!(output.contains(&format!(
|
||||
"telemt_build_info{{version=\"{}\"}} 1",
|
||||
@@ -3403,7 +3597,10 @@ mod tests {
|
||||
)));
|
||||
assert!(output.contains("telemt_connections_total 2"));
|
||||
assert!(output.contains("telemt_connections_bad_total 1"));
|
||||
assert!(output
|
||||
.contains("telemt_connections_bad_by_class_total{class=\"tls_handshake_bad_client\"} 1"));
|
||||
assert!(output.contains("telemt_handshake_timeouts_total 1"));
|
||||
assert!(output.contains("telemt_handshake_failures_by_class_total{class=\"timeout\"} 1"));
|
||||
assert!(output.contains("telemt_auth_expensive_checks_total 9"));
|
||||
assert!(output.contains("telemt_auth_budget_exhausted_total 2"));
|
||||
assert!(output.contains("telemt_upstream_connect_attempt_total 2"));
|
||||
@@ -3457,13 +3654,86 @@ mod tests {
|
||||
assert!(output.contains("telemt_ip_tracker_cleanup_queue_len 0"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_render_tls_front_profile_health() {
|
||||
let stats = Stats::new();
|
||||
let shared_state = ProxySharedState::new();
|
||||
let tracker = UserIpTracker::new();
|
||||
let mut config = ProxyConfig::default();
|
||||
config.censorship.tls_domain = "primary.example".to_string();
|
||||
config.censorship.tls_domains = vec!["fallback.example".to_string()];
|
||||
|
||||
let cache = TlsFrontCache::new(
|
||||
&[
|
||||
"primary.example".to_string(),
|
||||
"fallback.example".to_string(),
|
||||
],
|
||||
1024,
|
||||
"tlsfront-profile-health-test",
|
||||
);
|
||||
cache
|
||||
.set(
|
||||
"primary.example",
|
||||
CachedTlsData {
|
||||
server_hello_template: ParsedServerHello {
|
||||
version: [0x03, 0x03],
|
||||
random: [0u8; 32],
|
||||
session_id: Vec::new(),
|
||||
cipher_suite: [0x13, 0x01],
|
||||
compression: 0,
|
||||
extensions: Vec::new(),
|
||||
},
|
||||
cert_info: None,
|
||||
cert_payload: Some(TlsCertPayload {
|
||||
cert_chain_der: vec![vec![0x30, 0x01]],
|
||||
certificate_message: vec![0x0b, 0x00, 0x00, 0x00],
|
||||
}),
|
||||
app_data_records_sizes: vec![1024, 512],
|
||||
total_app_data_len: 1536,
|
||||
behavior_profile: TlsBehaviorProfile {
|
||||
change_cipher_spec_count: 1,
|
||||
app_data_record_sizes: vec![1024, 512],
|
||||
ticket_record_sizes: vec![69],
|
||||
source: TlsProfileSource::Merged,
|
||||
},
|
||||
fetched_at: SystemTime::now(),
|
||||
domain: "primary.example".to_string(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let output = render_metrics(&stats, &shared_state, &config, &tracker, Some(&cache)).await;
|
||||
|
||||
assert!(output.contains("telemt_tls_front_profile_domains{status=\"configured\"} 2"));
|
||||
assert!(output.contains("telemt_tls_front_profile_domains{status=\"emitted\"} 2"));
|
||||
assert!(output.contains("telemt_tls_front_profile_domains{status=\"suppressed\"} 0"));
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_info{domain=\"primary.example\",source=\"merged\",is_default=\"false\",has_cert_info=\"false\",has_cert_payload=\"true\"} 1")
|
||||
);
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_info{domain=\"fallback.example\",source=\"default\",is_default=\"true\",has_cert_info=\"false\",has_cert_payload=\"false\"} 1")
|
||||
);
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_app_data_records{domain=\"primary.example\"} 2")
|
||||
);
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_ticket_records{domain=\"primary.example\"} 1")
|
||||
);
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_change_cipher_spec_records{domain=\"primary.example\"} 1")
|
||||
);
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_app_data_bytes{domain=\"primary.example\"} 1536")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_render_empty_stats() {
|
||||
let stats = Stats::new();
|
||||
let shared_state = ProxySharedState::new();
|
||||
let tracker = UserIpTracker::new();
|
||||
let config = ProxyConfig::default();
|
||||
let output = render_metrics(&stats, &shared_state, &config, &tracker).await;
|
||||
let output = render_metrics(&stats, &shared_state, &config, &tracker, None).await;
|
||||
assert!(output.contains("telemt_connections_total 0"));
|
||||
assert!(output.contains("telemt_connections_bad_total 0"));
|
||||
assert!(output.contains("telemt_handshake_timeouts_total 0"));
|
||||
@@ -3487,7 +3757,7 @@ mod tests {
|
||||
let mut config = ProxyConfig::default();
|
||||
config.access.user_max_unique_ips_global_each = 2;
|
||||
|
||||
let output = render_metrics(&stats, &shared_state, &config, &tracker).await;
|
||||
let output = render_metrics(&stats, &shared_state, &config, &tracker, None).await;
|
||||
|
||||
assert!(output.contains("telemt_user_unique_ips_limit{user=\"alice\"} 2"));
|
||||
assert!(output.contains("telemt_user_unique_ips_utilization{user=\"alice\"} 0.500000"));
|
||||
@@ -3499,11 +3769,13 @@ mod tests {
|
||||
let shared_state = ProxySharedState::new();
|
||||
let tracker = UserIpTracker::new();
|
||||
let config = ProxyConfig::default();
|
||||
let output = render_metrics(&stats, &shared_state, &config, &tracker).await;
|
||||
let output = render_metrics(&stats, &shared_state, &config, &tracker, None).await;
|
||||
assert!(output.contains("# TYPE telemt_uptime_seconds gauge"));
|
||||
assert!(output.contains("# TYPE telemt_connections_total counter"));
|
||||
assert!(output.contains("# TYPE telemt_connections_bad_total counter"));
|
||||
assert!(output.contains("# TYPE telemt_connections_bad_by_class_total counter"));
|
||||
assert!(output.contains("# TYPE telemt_handshake_timeouts_total counter"));
|
||||
assert!(output.contains("# TYPE telemt_handshake_failures_by_class_total counter"));
|
||||
assert!(output.contains("# TYPE telemt_auth_expensive_checks_total counter"));
|
||||
assert!(output.contains("# TYPE telemt_auth_budget_exhausted_total counter"));
|
||||
assert!(output.contains("# TYPE telemt_upstream_connect_attempt_total counter"));
|
||||
@@ -3546,6 +3818,15 @@ mod tests {
|
||||
assert!(
|
||||
output.contains("# TYPE telemt_tls_front_full_cert_budget_cap_drops_total counter")
|
||||
);
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_domains gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_info gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_age_seconds gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_app_data_records gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_ticket_records gauge"));
|
||||
assert!(
|
||||
output.contains("# TYPE telemt_tls_front_profile_change_cipher_spec_records gauge")
|
||||
);
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_app_data_bytes gauge"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3566,6 +3847,7 @@ mod tests {
|
||||
&beobachten,
|
||||
shared_state.as_ref(),
|
||||
&tracker,
|
||||
None,
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
@@ -3600,6 +3882,7 @@ mod tests {
|
||||
&beobachten,
|
||||
shared_state.as_ref(),
|
||||
&tracker,
|
||||
None,
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
@@ -3617,6 +3900,7 @@ mod tests {
|
||||
&beobachten,
|
||||
shared_state.as_ref(),
|
||||
&tracker,
|
||||
None,
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::config::ProxyConfig;
|
||||
use crate::network::dns_overrides::resolve_socket_addr;
|
||||
use crate::protocol::tls;
|
||||
use crate::stats::beobachten::BeobachtenStore;
|
||||
use crate::transport::proxy_protocol::{ProxyProtocolV1Builder, ProxyProtocolV2Builder};
|
||||
#[cfg(unix)]
|
||||
@@ -328,6 +329,89 @@ async fn wait_mask_outcome_budget(started: Instant, config: &ProxyConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tls_domain_mask_host_tests {
|
||||
use super::{mask_host_for_initial_data, matching_tls_domain_for_sni};
|
||||
use crate::config::ProxyConfig;
|
||||
|
||||
fn client_hello_with_sni(sni_host: &str) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(&[0x03, 0x03]);
|
||||
body.extend_from_slice(&[0u8; 32]);
|
||||
body.push(32);
|
||||
body.extend_from_slice(&[0x42u8; 32]);
|
||||
body.extend_from_slice(&2u16.to_be_bytes());
|
||||
body.extend_from_slice(&[0x13, 0x01]);
|
||||
body.push(1);
|
||||
body.push(0);
|
||||
|
||||
let host_bytes = sni_host.as_bytes();
|
||||
let mut sni_payload = Vec::new();
|
||||
sni_payload.extend_from_slice(&((host_bytes.len() + 3) as u16).to_be_bytes());
|
||||
sni_payload.push(0);
|
||||
sni_payload.extend_from_slice(&(host_bytes.len() as u16).to_be_bytes());
|
||||
sni_payload.extend_from_slice(host_bytes);
|
||||
|
||||
let mut extensions = Vec::new();
|
||||
extensions.extend_from_slice(&0x0000u16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(sni_payload.len() as u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&sni_payload);
|
||||
body.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
|
||||
body.extend_from_slice(&extensions);
|
||||
|
||||
let mut handshake = Vec::new();
|
||||
handshake.push(0x01);
|
||||
let body_len = (body.len() as u32).to_be_bytes();
|
||||
handshake.extend_from_slice(&body_len[1..4]);
|
||||
handshake.extend_from_slice(&body);
|
||||
|
||||
let mut record = Vec::new();
|
||||
record.push(0x16);
|
||||
record.extend_from_slice(&[0x03, 0x01]);
|
||||
record.extend_from_slice(&(handshake.len() as u16).to_be_bytes());
|
||||
record.extend_from_slice(&handshake);
|
||||
record
|
||||
}
|
||||
|
||||
fn config_with_tls_domains() -> ProxyConfig {
|
||||
let mut config = ProxyConfig::default();
|
||||
config.censorship.tls_domain = "a.com".to_string();
|
||||
config.censorship.tls_domains = vec!["b.com".to_string(), "c.com".to_string()];
|
||||
config.censorship.mask_host = Some("a.com".to_string());
|
||||
config
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching_tls_domain_accepts_primary_and_extra_domains_case_insensitively() {
|
||||
let config = config_with_tls_domains();
|
||||
|
||||
assert_eq!(matching_tls_domain_for_sni(&config, "A.COM"), Some("a.com"));
|
||||
assert_eq!(matching_tls_domain_for_sni(&config, "B.COM"), Some("b.com"));
|
||||
assert_eq!(matching_tls_domain_for_sni(&config, "unknown.com"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_host_preserves_explicit_non_primary_origin() {
|
||||
let mut config = config_with_tls_domains();
|
||||
config.censorship.mask_host = Some("origin.example".to_string());
|
||||
|
||||
let initial_data = client_hello_with_sni("b.com");
|
||||
|
||||
assert_eq!(
|
||||
mask_host_for_initial_data(&config, &initial_data),
|
||||
"origin.example"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_host_uses_matching_tls_domain_when_mask_host_is_primary_default() {
|
||||
let config = config_with_tls_domains();
|
||||
let initial_data = client_hello_with_sni("b.com");
|
||||
|
||||
assert_eq!(mask_host_for_initial_data(&config, &initial_data), "b.com");
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect client type based on initial data
|
||||
fn detect_client_type(data: &[u8]) -> &'static str {
|
||||
// Check for HTTP request
|
||||
@@ -360,6 +444,37 @@ fn parse_mask_host_ip_literal(host: &str) -> Option<IpAddr> {
|
||||
host.parse::<IpAddr>().ok()
|
||||
}
|
||||
|
||||
fn matching_tls_domain_for_sni<'a>(config: &'a ProxyConfig, sni: &str) -> Option<&'a str> {
|
||||
if config.censorship.tls_domain.eq_ignore_ascii_case(sni) {
|
||||
return Some(config.censorship.tls_domain.as_str());
|
||||
}
|
||||
|
||||
for domain in &config.censorship.tls_domains {
|
||||
if domain.eq_ignore_ascii_case(sni) {
|
||||
return Some(domain.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn mask_host_for_initial_data<'a>(config: &'a ProxyConfig, initial_data: &[u8]) -> &'a str {
|
||||
let configured_mask_host = config
|
||||
.censorship
|
||||
.mask_host
|
||||
.as_deref()
|
||||
.unwrap_or(&config.censorship.tls_domain);
|
||||
|
||||
if !configured_mask_host.eq_ignore_ascii_case(&config.censorship.tls_domain) {
|
||||
return configured_mask_host;
|
||||
}
|
||||
|
||||
tls::extract_sni_from_client_hello(initial_data)
|
||||
.as_deref()
|
||||
.and_then(|sni| matching_tls_domain_for_sni(config, sni))
|
||||
.unwrap_or(configured_mask_host)
|
||||
}
|
||||
|
||||
fn canonical_ip(ip: IpAddr) -> IpAddr {
|
||||
match ip {
|
||||
IpAddr::V6(v6) => v6
|
||||
@@ -734,11 +849,7 @@ pub async fn handle_bad_client<R, W>(
|
||||
return;
|
||||
}
|
||||
|
||||
let mask_host = config
|
||||
.censorship
|
||||
.mask_host
|
||||
.as_deref()
|
||||
.unwrap_or(&config.censorship.tls_domain);
|
||||
let mask_host = mask_host_for_initial_data(config, initial_data);
|
||||
let mask_port = config.censorship.mask_port;
|
||||
|
||||
// Fail closed when fallback points at our own listener endpoint.
|
||||
|
||||
@@ -12,7 +12,7 @@ use tokio::time::sleep;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsFetchResult,
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsFetchResult, TlsProfileSource,
|
||||
};
|
||||
|
||||
const FULL_CERT_SENT_SWEEP_INTERVAL_SECS: u64 = 30;
|
||||
@@ -42,6 +42,30 @@ pub struct TlsFrontCache {
|
||||
disk_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Read-only health view for one configured TLS front domain.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TlsFrontProfileHealth {
|
||||
pub(crate) domain: String,
|
||||
pub(crate) source: &'static str,
|
||||
pub(crate) age_seconds: u64,
|
||||
pub(crate) is_default: bool,
|
||||
pub(crate) has_cert_info: bool,
|
||||
pub(crate) has_cert_payload: bool,
|
||||
pub(crate) app_data_records: usize,
|
||||
pub(crate) ticket_records: usize,
|
||||
pub(crate) change_cipher_spec_count: u8,
|
||||
pub(crate) total_app_data_len: usize,
|
||||
}
|
||||
|
||||
fn profile_source_label(source: TlsProfileSource) -> &'static str {
|
||||
match source {
|
||||
TlsProfileSource::Default => "default",
|
||||
TlsProfileSource::Raw => "raw",
|
||||
TlsProfileSource::Rustls => "rustls",
|
||||
TlsProfileSource::Merged => "merged",
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TlsFrontCache {
|
||||
pub fn new(domains: &[String], default_len: usize, disk_path: impl AsRef<Path>) -> Self {
|
||||
@@ -93,6 +117,51 @@ impl TlsFrontCache {
|
||||
self.memory.read().await.contains_key(domain)
|
||||
}
|
||||
|
||||
pub(crate) async fn profile_health_snapshot(
|
||||
&self,
|
||||
domains: &[String],
|
||||
max_domains: usize,
|
||||
) -> (Vec<TlsFrontProfileHealth>, usize) {
|
||||
let guard = self.memory.read().await;
|
||||
let now = SystemTime::now();
|
||||
let mut snapshot = Vec::with_capacity(domains.len().min(max_domains));
|
||||
let mut suppressed = 0usize;
|
||||
|
||||
for domain in domains {
|
||||
if snapshot.len() >= max_domains {
|
||||
suppressed = suppressed.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let cached = guard
|
||||
.get(domain)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.default.clone());
|
||||
let behavior = &cached.behavior_profile;
|
||||
let age_seconds = now
|
||||
.duration_since(cached.fetched_at)
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
snapshot.push(TlsFrontProfileHealth {
|
||||
domain: domain.clone(),
|
||||
source: profile_source_label(behavior.source),
|
||||
age_seconds,
|
||||
is_default: cached.domain == "default",
|
||||
has_cert_info: cached.cert_info.is_some(),
|
||||
has_cert_payload: cached.cert_payload.is_some(),
|
||||
app_data_records: cached.app_data_records_sizes.len().max(
|
||||
behavior.app_data_record_sizes.len(),
|
||||
),
|
||||
ticket_records: behavior.ticket_record_sizes.len(),
|
||||
change_cipher_spec_count: behavior.change_cipher_spec_count,
|
||||
total_app_data_len: cached.total_app_data_len,
|
||||
});
|
||||
}
|
||||
|
||||
(snapshot, suppressed)
|
||||
}
|
||||
|
||||
fn full_cert_sent_shard_index(client_ip: IpAddr) -> usize {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
client_ip.hash(&mut hasher);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user