mirror of
https://github.com/telemt/telemt.git
synced 2026-06-19 01:11:09 +03:00
Compare commits
13 Commits
3.3.29
...
6e794f17e5
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e794f17e5 | |||
| 3eb384e02a | |||
| 7b570be5b3 | |||
| 0461bc65c6 | |||
| cf82b637d2 | |||
| 2e8bfa1101 | |||
| d091b0b251 | |||
| 56fc6c4896 | |||
| 95685adba7 | |||
| 909714af31 | |||
| dc2b4395bd | |||
| 39875afbff | |||
| 2ea7813ed4 |
@@ -7,7 +7,16 @@ queries:
|
|||||||
- uses: security-and-quality
|
- uses: security-and-quality
|
||||||
- uses: ./.github/codeql/queries
|
- uses: ./.github/codeql/queries
|
||||||
|
|
||||||
|
paths-ignore:
|
||||||
|
- "**/tests/**"
|
||||||
|
- "**/test/**"
|
||||||
|
- "**/*_test.rs"
|
||||||
|
- "**/*/tests.rs"
|
||||||
query-filters:
|
query-filters:
|
||||||
|
- exclude:
|
||||||
|
tags:
|
||||||
|
- test
|
||||||
|
|
||||||
- exclude:
|
- exclude:
|
||||||
id:
|
id:
|
||||||
- rust/unwrap-on-option
|
- rust/unwrap-on-option
|
||||||
|
|||||||
+49
-45
@@ -1,8 +1,8 @@
|
|||||||
# Code of Conduct
|
# Code of Conduct
|
||||||
|
|
||||||
## 1. Purpose
|
## Purpose
|
||||||
|
|
||||||
Telemt exists to solve technical problems.
|
**Telemt exists to solve technical problems.**
|
||||||
|
|
||||||
Telemt is open to contributors who want to learn, improve and build meaningful systems together.
|
Telemt is open to contributors who want to learn, improve and build meaningful systems together.
|
||||||
|
|
||||||
@@ -18,27 +18,34 @@ Technology has consequences. Responsibility is inherent.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Principles
|
## Principles
|
||||||
|
|
||||||
* **Technical over emotional**
|
* **Technical over emotional**
|
||||||
|
|
||||||
Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
|
Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
|
||||||
|
|
||||||
* **Clarity over noise**
|
* **Clarity over noise**
|
||||||
|
|
||||||
Communication is structured, concise, and relevant.
|
Communication is structured, concise, and relevant.
|
||||||
|
|
||||||
* **Openness with standards**
|
* **Openness with standards**
|
||||||
|
|
||||||
Participation is open. The work remains disciplined.
|
Participation is open. The work remains disciplined.
|
||||||
|
|
||||||
* **Independence of judgment**
|
* **Independence of judgment**
|
||||||
|
|
||||||
Claims are evaluated on technical merit, not affiliation or posture.
|
Claims are evaluated on technical merit, not affiliation or posture.
|
||||||
|
|
||||||
* **Responsibility over capability**
|
* **Responsibility over capability**
|
||||||
|
|
||||||
Capability does not justify careless use.
|
Capability does not justify careless use.
|
||||||
|
|
||||||
* **Cooperation over friction**
|
* **Cooperation over friction**
|
||||||
|
|
||||||
Progress depends on coordination, mutual support, and honest review.
|
Progress depends on coordination, mutual support, and honest review.
|
||||||
|
|
||||||
* **Good intent, rigorous method**
|
* **Good intent, rigorous method**
|
||||||
|
|
||||||
Assume good intent, but require rigor.
|
Assume good intent, but require rigor.
|
||||||
|
|
||||||
> **Aussagen gelten nach ihrer Begründung.**
|
> **Aussagen gelten nach ihrer Begründung.**
|
||||||
@@ -47,7 +54,7 @@ Technology has consequences. Responsibility is inherent.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Expected Behavior
|
## Expected Behavior
|
||||||
|
|
||||||
Participants are expected to:
|
Participants are expected to:
|
||||||
|
|
||||||
@@ -69,7 +76,7 @@ New contributors are welcome. They are expected to grow into these standards. Ex
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Unacceptable Behavior
|
## Unacceptable Behavior
|
||||||
|
|
||||||
The following is not allowed:
|
The following is not allowed:
|
||||||
|
|
||||||
@@ -89,7 +96,7 @@ Such discussions may be closed, removed, or redirected.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Security and Misuse
|
## Security and Misuse
|
||||||
|
|
||||||
Telemt is intended for responsible use.
|
Telemt is intended for responsible use.
|
||||||
|
|
||||||
@@ -109,15 +116,13 @@ Security is both technical and behavioral.
|
|||||||
|
|
||||||
Telemt is open to contributors of different backgrounds, experience levels, and working styles.
|
Telemt is open to contributors of different backgrounds, experience levels, and working styles.
|
||||||
|
|
||||||
Standards are public, legible, and applied to the work itself.
|
- Standards are public, legible, and applied to the work itself.
|
||||||
|
- Questions are welcome. Careful disagreement is welcome. Honest correction is welcome.
|
||||||
Questions are welcome. Careful disagreement is welcome. Honest correction is welcome.
|
- Gatekeeping by obscurity, status signaling, or hostility is not.
|
||||||
|
|
||||||
Gatekeeping by obscurity, status signaling, or hostility is not.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies to all official spaces:
|
This Code of Conduct applies to all official spaces:
|
||||||
|
|
||||||
@@ -127,16 +132,19 @@ This Code of Conduct applies to all official spaces:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Maintainer Stewardship
|
## Maintainer Stewardship
|
||||||
|
|
||||||
Maintainers are responsible for final decisions in matters of conduct, scope, and direction.
|
Maintainers are responsible for final decisions in matters of conduct, scope, and direction.
|
||||||
|
|
||||||
This responsibility is stewardship: preserving continuity, protecting signal, maintaining standards, and keeping Telemt workable for others.
|
This responsibility is stewardship:
|
||||||
|
- preserving continuity,
|
||||||
|
- protecting signal,
|
||||||
|
- maintaining standards,
|
||||||
|
- keeping Telemt workable for others.
|
||||||
|
|
||||||
Judgment should be exercised with restraint, consistency, and institutional responsibility.
|
Judgment should be exercised with restraint, consistency, and institutional responsibility.
|
||||||
|
- Not every decision requires extended debate.
|
||||||
Not every decision requires extended debate.
|
- Not every intervention requires public explanation.
|
||||||
Not every intervention requires public explanation.
|
|
||||||
|
|
||||||
All decisions are expected to serve the durability, clarity, and integrity of Telemt.
|
All decisions are expected to serve the durability, clarity, and integrity of Telemt.
|
||||||
|
|
||||||
@@ -146,7 +154,7 @@ All decisions are expected to serve the durability, clarity, and integrity of Te
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Maintainers may act to preserve the integrity of Telemt, including by:
|
Maintainers may act to preserve the integrity of Telemt, including by:
|
||||||
|
|
||||||
@@ -156,44 +164,40 @@ Maintainers may act to preserve the integrity of Telemt, including by:
|
|||||||
* Restricting or banning participants
|
* Restricting or banning participants
|
||||||
|
|
||||||
Actions are taken to maintain function, continuity, and signal quality.
|
Actions are taken to maintain function, continuity, and signal quality.
|
||||||
|
- Where possible, correction is preferred to exclusion.
|
||||||
Where possible, correction is preferred to exclusion.
|
- Where necessary, exclusion is preferred to decay.
|
||||||
|
|
||||||
Where necessary, exclusion is preferred to decay.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Final
|
## Final
|
||||||
|
|
||||||
Telemt is built on discipline, structure, and shared intent.
|
Telemt is built on discipline, structure, and shared intent.
|
||||||
|
- Signal over noise.
|
||||||
|
- Facts over opinion.
|
||||||
|
- Systems over rhetoric.
|
||||||
|
|
||||||
Signal over noise.
|
- Work is collective.
|
||||||
Facts over opinion.
|
- Outcomes are shared.
|
||||||
Systems over rhetoric.
|
- Responsibility is distributed.
|
||||||
|
|
||||||
Work is collective.
|
- Precision is learned.
|
||||||
Outcomes are shared.
|
- Rigor is expected.
|
||||||
Responsibility is distributed.
|
- Help is part of the work.
|
||||||
|
|
||||||
Precision is learned.
|
|
||||||
Rigor is expected.
|
|
||||||
Help is part of the work.
|
|
||||||
|
|
||||||
> **Ordnung ist Voraussetzung der Freiheit.**
|
> **Ordnung ist Voraussetzung der Freiheit.**
|
||||||
|
|
||||||
If you contribute — contribute with care.
|
- If you contribute — contribute with care.
|
||||||
If you speak — speak with substance.
|
- If you speak — speak with substance.
|
||||||
If you engage — engage constructively.
|
- If you engage — engage constructively.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. After All
|
## After All
|
||||||
|
|
||||||
Systems outlive intentions.
|
Systems outlive intentions.
|
||||||
|
- What is built will be used.
|
||||||
What is built will be used.
|
- What is released will propagate.
|
||||||
What is released will propagate.
|
- What is maintained will define the future state.
|
||||||
What is maintained will define the future state.
|
|
||||||
|
|
||||||
There is no neutral infrastructure, only infrastructure shaped well or poorly.
|
There is no neutral infrastructure, only infrastructure shaped well or poorly.
|
||||||
|
|
||||||
@@ -201,8 +205,8 @@ There is no neutral infrastructure, only infrastructure shaped well or poorly.
|
|||||||
|
|
||||||
> Every system carries responsibility.
|
> Every system carries responsibility.
|
||||||
|
|
||||||
Stability requires discipline.
|
- Stability requires discipline.
|
||||||
Freedom requires structure.
|
- Freedom requires structure.
|
||||||
Trust requires honesty.
|
- Trust requires honesty.
|
||||||
|
|
||||||
In the end, the system reflects its contributors.
|
In the end: the system reflects its contributors.
|
||||||
|
|||||||
Generated
+13
@@ -2822,6 +2822,7 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
@@ -3148,6 +3149,18 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-appender"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.31"
|
version = "0.1.31"
|
||||||
|
|||||||
+16
-3
@@ -27,7 +27,13 @@ static_assertions = "1.1"
|
|||||||
|
|
||||||
# Network
|
# Network
|
||||||
socket2 = { version = "0.6", features = ["all"] }
|
socket2 = { version = "0.6", features = ["all"] }
|
||||||
nix = { version = "0.31", default-features = false, features = ["net", "fs"] }
|
nix = { version = "0.31", default-features = false, features = [
|
||||||
|
"net",
|
||||||
|
"user",
|
||||||
|
"process",
|
||||||
|
"fs",
|
||||||
|
"signal",
|
||||||
|
] }
|
||||||
shadowsocks = { version = "1.24", features = ["aead-cipher-2022"] }
|
shadowsocks = { version = "1.24", features = ["aead-cipher-2022"] }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
@@ -41,6 +47,7 @@ bytes = "1.9"
|
|||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
tracing-appender = "0.2"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
dashmap = "6.1"
|
dashmap = "6.1"
|
||||||
arc-swap = "1.7"
|
arc-swap = "1.7"
|
||||||
@@ -65,8 +72,14 @@ hyper = { version = "1", features = ["server", "http1"] }
|
|||||||
hyper-util = { version = "0.1", features = ["tokio", "server-auto"] }
|
hyper-util = { version = "0.1", features = ["tokio", "server-auto"] }
|
||||||
http-body-util = "0.1"
|
http-body-util = "0.1"
|
||||||
httpdate = "1.0"
|
httpdate = "1.0"
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = [
|
||||||
rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"] }
|
"tls12",
|
||||||
|
] }
|
||||||
|
rustls = { version = "0.23", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"tls12",
|
||||||
|
"ring",
|
||||||
|
] }
|
||||||
webpki-roots = "1.0"
|
webpki-roots = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
+16
-2
@@ -28,9 +28,23 @@ RUN cargo build --release && strip target/release/telemt
|
|||||||
FROM debian:12-slim AS minimal
|
FROM debian:12-slim AS minimal
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
upx \
|
|
||||||
binutils \
|
binutils \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
\
|
||||||
|
# install UPX from Telemt releases
|
||||||
|
&& curl -fL \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 3 \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--max-time 120 \
|
||||||
|
-o /tmp/upx.tar.xz \
|
||||||
|
https://github.com/telemt/telemt/releases/download/toolchains/upx-amd64_linux.tar.xz \
|
||||||
|
&& tar -xf /tmp/upx.tar.xz -C /tmp \
|
||||||
|
&& mv /tmp/upx*/upx /usr/local/bin/upx \
|
||||||
|
&& chmod +x /usr/local/bin/upx \
|
||||||
|
&& rm -rf /tmp/upx*
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/telemt /telemt
|
COPY --from=builder /build/target/release/telemt /telemt
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,24 @@ pub(super) struct ZeroMiddleProxyData {
|
|||||||
pub(super) route_drop_queue_full_total: u64,
|
pub(super) route_drop_queue_full_total: u64,
|
||||||
pub(super) route_drop_queue_full_base_total: u64,
|
pub(super) route_drop_queue_full_base_total: u64,
|
||||||
pub(super) route_drop_queue_full_high_total: u64,
|
pub(super) route_drop_queue_full_high_total: u64,
|
||||||
|
pub(super) d2c_batches_total: u64,
|
||||||
|
pub(super) d2c_batch_frames_total: u64,
|
||||||
|
pub(super) d2c_batch_bytes_total: u64,
|
||||||
|
pub(super) d2c_flush_reason_queue_drain_total: u64,
|
||||||
|
pub(super) d2c_flush_reason_batch_frames_total: u64,
|
||||||
|
pub(super) d2c_flush_reason_batch_bytes_total: u64,
|
||||||
|
pub(super) d2c_flush_reason_max_delay_total: u64,
|
||||||
|
pub(super) d2c_flush_reason_ack_immediate_total: u64,
|
||||||
|
pub(super) d2c_flush_reason_close_total: u64,
|
||||||
|
pub(super) d2c_data_frames_total: u64,
|
||||||
|
pub(super) d2c_ack_frames_total: u64,
|
||||||
|
pub(super) d2c_payload_bytes_total: u64,
|
||||||
|
pub(super) d2c_write_mode_coalesced_total: u64,
|
||||||
|
pub(super) d2c_write_mode_split_total: u64,
|
||||||
|
pub(super) d2c_quota_reject_pre_write_total: u64,
|
||||||
|
pub(super) d2c_quota_reject_post_write_total: u64,
|
||||||
|
pub(super) d2c_frame_buf_shrink_total: u64,
|
||||||
|
pub(super) d2c_frame_buf_shrink_bytes_total: u64,
|
||||||
pub(super) socks_kdf_strict_reject_total: u64,
|
pub(super) socks_kdf_strict_reject_total: u64,
|
||||||
pub(super) socks_kdf_compat_fallback_total: u64,
|
pub(super) socks_kdf_compat_fallback_total: u64,
|
||||||
pub(super) endpoint_quarantine_total: u64,
|
pub(super) endpoint_quarantine_total: u64,
|
||||||
|
|||||||
@@ -68,6 +68,25 @@ pub(super) fn build_zero_all_data(stats: &Stats, configured_users: usize) -> Zer
|
|||||||
route_drop_queue_full_total: stats.get_me_route_drop_queue_full(),
|
route_drop_queue_full_total: stats.get_me_route_drop_queue_full(),
|
||||||
route_drop_queue_full_base_total: stats.get_me_route_drop_queue_full_base(),
|
route_drop_queue_full_base_total: stats.get_me_route_drop_queue_full_base(),
|
||||||
route_drop_queue_full_high_total: stats.get_me_route_drop_queue_full_high(),
|
route_drop_queue_full_high_total: stats.get_me_route_drop_queue_full_high(),
|
||||||
|
d2c_batches_total: stats.get_me_d2c_batches_total(),
|
||||||
|
d2c_batch_frames_total: stats.get_me_d2c_batch_frames_total(),
|
||||||
|
d2c_batch_bytes_total: stats.get_me_d2c_batch_bytes_total(),
|
||||||
|
d2c_flush_reason_queue_drain_total: stats.get_me_d2c_flush_reason_queue_drain_total(),
|
||||||
|
d2c_flush_reason_batch_frames_total: stats.get_me_d2c_flush_reason_batch_frames_total(),
|
||||||
|
d2c_flush_reason_batch_bytes_total: stats.get_me_d2c_flush_reason_batch_bytes_total(),
|
||||||
|
d2c_flush_reason_max_delay_total: stats.get_me_d2c_flush_reason_max_delay_total(),
|
||||||
|
d2c_flush_reason_ack_immediate_total: stats
|
||||||
|
.get_me_d2c_flush_reason_ack_immediate_total(),
|
||||||
|
d2c_flush_reason_close_total: stats.get_me_d2c_flush_reason_close_total(),
|
||||||
|
d2c_data_frames_total: stats.get_me_d2c_data_frames_total(),
|
||||||
|
d2c_ack_frames_total: stats.get_me_d2c_ack_frames_total(),
|
||||||
|
d2c_payload_bytes_total: stats.get_me_d2c_payload_bytes_total(),
|
||||||
|
d2c_write_mode_coalesced_total: stats.get_me_d2c_write_mode_coalesced_total(),
|
||||||
|
d2c_write_mode_split_total: stats.get_me_d2c_write_mode_split_total(),
|
||||||
|
d2c_quota_reject_pre_write_total: stats.get_me_d2c_quota_reject_pre_write_total(),
|
||||||
|
d2c_quota_reject_post_write_total: stats.get_me_d2c_quota_reject_post_write_total(),
|
||||||
|
d2c_frame_buf_shrink_total: stats.get_me_d2c_frame_buf_shrink_total(),
|
||||||
|
d2c_frame_buf_shrink_bytes_total: stats.get_me_d2c_frame_buf_shrink_bytes_total(),
|
||||||
socks_kdf_strict_reject_total: stats.get_me_socks_kdf_strict_reject(),
|
socks_kdf_strict_reject_total: stats.get_me_socks_kdf_strict_reject(),
|
||||||
socks_kdf_compat_fallback_total: stats.get_me_socks_kdf_compat_fallback(),
|
socks_kdf_compat_fallback_total: stats.get_me_socks_kdf_compat_fallback(),
|
||||||
endpoint_quarantine_total: stats.get_me_endpoint_quarantine_total(),
|
endpoint_quarantine_total: stats.get_me_endpoint_quarantine_total(),
|
||||||
|
|||||||
+418
-70
@@ -1,11 +1,270 @@
|
|||||||
//! CLI commands: --init (fire-and-forget setup)
|
//! CLI commands: --init (fire-and-forget setup), daemon options, subcommands
|
||||||
|
//!
|
||||||
|
//! Subcommands:
|
||||||
|
//! - `start [OPTIONS] [config.toml]` - Start the daemon
|
||||||
|
//! - `stop [--pid-file PATH]` - Stop a running daemon
|
||||||
|
//! - `reload [--pid-file PATH]` - Reload configuration (SIGHUP)
|
||||||
|
//! - `status [--pid-file PATH]` - Check daemon status
|
||||||
|
//! - `run [OPTIONS] [config.toml]` - Run in foreground (default behavior)
|
||||||
|
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use crate::daemon::{self, DaemonOptions, DEFAULT_PID_FILE};
|
||||||
|
|
||||||
|
/// CLI subcommand to execute.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Subcommand {
|
||||||
|
/// Run the proxy (default, or explicit `run` subcommand).
|
||||||
|
Run,
|
||||||
|
/// Start as daemon (`start` subcommand).
|
||||||
|
Start,
|
||||||
|
/// Stop a running daemon (`stop` subcommand).
|
||||||
|
Stop,
|
||||||
|
/// Reload configuration (`reload` subcommand).
|
||||||
|
Reload,
|
||||||
|
/// Check daemon status (`status` subcommand).
|
||||||
|
Status,
|
||||||
|
/// Fire-and-forget setup (`--init`).
|
||||||
|
Init,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed subcommand with its options.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ParsedCommand {
|
||||||
|
pub subcommand: Subcommand,
|
||||||
|
pub pid_file: PathBuf,
|
||||||
|
pub config_path: String,
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub daemon_opts: DaemonOptions,
|
||||||
|
pub init_opts: Option<InitOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ParsedCommand {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
subcommand: Subcommand::Run,
|
||||||
|
#[cfg(unix)]
|
||||||
|
pid_file: PathBuf::from(DEFAULT_PID_FILE),
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pid_file: PathBuf::from("/var/run/telemt.pid"),
|
||||||
|
config_path: "config.toml".to_string(),
|
||||||
|
#[cfg(unix)]
|
||||||
|
daemon_opts: DaemonOptions::default(),
|
||||||
|
init_opts: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse CLI arguments into a command structure.
|
||||||
|
pub fn parse_command(args: &[String]) -> ParsedCommand {
|
||||||
|
let mut cmd = ParsedCommand::default();
|
||||||
|
|
||||||
|
// Check for --init first (legacy form)
|
||||||
|
if args.iter().any(|a| a == "--init") {
|
||||||
|
cmd.subcommand = Subcommand::Init;
|
||||||
|
cmd.init_opts = parse_init_args(args);
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for subcommand as first argument
|
||||||
|
if let Some(first) = args.first() {
|
||||||
|
match first.as_str() {
|
||||||
|
"start" => {
|
||||||
|
cmd.subcommand = Subcommand::Start;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
cmd.daemon_opts = parse_daemon_args(args);
|
||||||
|
// Force daemonize for start command
|
||||||
|
cmd.daemon_opts.daemonize = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"stop" => {
|
||||||
|
cmd.subcommand = Subcommand::Stop;
|
||||||
|
}
|
||||||
|
"reload" => {
|
||||||
|
cmd.subcommand = Subcommand::Reload;
|
||||||
|
}
|
||||||
|
"status" => {
|
||||||
|
cmd.subcommand = Subcommand::Status;
|
||||||
|
}
|
||||||
|
"run" => {
|
||||||
|
cmd.subcommand = Subcommand::Run;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
cmd.daemon_opts = parse_daemon_args(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// No subcommand, default to Run
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
cmd.daemon_opts = parse_daemon_args(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse remaining options
|
||||||
|
let mut i = 0;
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
// Skip subcommand names
|
||||||
|
"start" | "stop" | "reload" | "status" | "run" => {}
|
||||||
|
// PID file option (for stop/reload/status)
|
||||||
|
"--pid-file" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
cmd.pid_file = PathBuf::from(&args[i]);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
cmd.daemon_opts.pid_file = Some(cmd.pid_file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--pid-file=") => {
|
||||||
|
cmd.pid_file = PathBuf::from(s.trim_start_matches("--pid-file="));
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
cmd.daemon_opts.pid_file = Some(cmd.pid_file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Config path (positional, non-flag argument)
|
||||||
|
s if !s.starts_with('-') => {
|
||||||
|
cmd.config_path = s.to_string();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a subcommand that doesn't require starting the server.
|
||||||
|
/// Returns `Some(exit_code)` if the command was handled, `None` if server should start.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
|
||||||
|
match cmd.subcommand {
|
||||||
|
Subcommand::Stop => Some(cmd_stop(&cmd.pid_file)),
|
||||||
|
Subcommand::Reload => Some(cmd_reload(&cmd.pid_file)),
|
||||||
|
Subcommand::Status => Some(cmd_status(&cmd.pid_file)),
|
||||||
|
Subcommand::Init => {
|
||||||
|
if let Some(opts) = cmd.init_opts.clone() {
|
||||||
|
match run_init(opts) {
|
||||||
|
Ok(()) => Some(0),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[telemt] Init failed: {}", e);
|
||||||
|
Some(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Run and Start need the server
|
||||||
|
Subcommand::Run | Subcommand::Start => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
|
||||||
|
match cmd.subcommand {
|
||||||
|
Subcommand::Stop | Subcommand::Reload | Subcommand::Status => {
|
||||||
|
eprintln!("[telemt] Subcommand not supported on this platform");
|
||||||
|
Some(1)
|
||||||
|
}
|
||||||
|
Subcommand::Init => {
|
||||||
|
if let Some(opts) = cmd.init_opts.clone() {
|
||||||
|
match run_init(opts) {
|
||||||
|
Ok(()) => Some(0),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[telemt] Init failed: {}", e);
|
||||||
|
Some(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Subcommand::Run | Subcommand::Start => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop command: send SIGTERM to the running daemon.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn cmd_stop(pid_file: &Path) -> i32 {
|
||||||
|
use nix::sys::signal::Signal;
|
||||||
|
|
||||||
|
println!("Stopping telemt daemon...");
|
||||||
|
|
||||||
|
match daemon::signal_pid_file(pid_file, Signal::SIGTERM) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("Stop signal sent successfully");
|
||||||
|
|
||||||
|
// Wait for process to exit (up to 10 seconds)
|
||||||
|
for _ in 0..20 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
|
if let daemon::DaemonStatus::NotRunning = daemon::check_status(pid_file) {
|
||||||
|
println!("Daemon stopped");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Daemon may still be shutting down");
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to stop daemon: {}", e);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload command: send SIGHUP to trigger config reload.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn cmd_reload(pid_file: &Path) -> i32 {
|
||||||
|
use nix::sys::signal::Signal;
|
||||||
|
|
||||||
|
println!("Reloading telemt configuration...");
|
||||||
|
|
||||||
|
match daemon::signal_pid_file(pid_file, Signal::SIGHUP) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("Reload signal sent successfully");
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to reload daemon: {}", e);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Status command: check if daemon is running.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn cmd_status(pid_file: &Path) -> i32 {
|
||||||
|
match daemon::check_status(pid_file) {
|
||||||
|
daemon::DaemonStatus::Running(pid) => {
|
||||||
|
println!("telemt is running (pid {})", pid);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
daemon::DaemonStatus::Stale(pid) => {
|
||||||
|
println!("telemt is not running (stale pid file, was pid {})", pid);
|
||||||
|
// Clean up stale PID file
|
||||||
|
let _ = std::fs::remove_file(pid_file);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
daemon::DaemonStatus::NotRunning => {
|
||||||
|
println!("telemt is not running");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Options for the init command
|
/// Options for the init command
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct InitOptions {
|
pub struct InitOptions {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
@@ -15,6 +274,64 @@ pub struct InitOptions {
|
|||||||
pub no_start: bool,
|
pub no_start: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse daemon-related options from CLI args.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn parse_daemon_args(args: &[String]) -> DaemonOptions {
|
||||||
|
let mut opts = DaemonOptions::default();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
"--daemon" | "-d" => {
|
||||||
|
opts.daemonize = true;
|
||||||
|
}
|
||||||
|
"--foreground" | "-f" => {
|
||||||
|
opts.foreground = true;
|
||||||
|
}
|
||||||
|
"--pid-file" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
opts.pid_file = Some(PathBuf::from(&args[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--pid-file=") => {
|
||||||
|
opts.pid_file = Some(PathBuf::from(s.trim_start_matches("--pid-file=")));
|
||||||
|
}
|
||||||
|
"--run-as-user" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
opts.user = Some(args[i].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--run-as-user=") => {
|
||||||
|
opts.user = Some(s.trim_start_matches("--run-as-user=").to_string());
|
||||||
|
}
|
||||||
|
"--run-as-group" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
opts.group = Some(args[i].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--run-as-group=") => {
|
||||||
|
opts.group = Some(s.trim_start_matches("--run-as-group=").to_string());
|
||||||
|
}
|
||||||
|
"--working-dir" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
opts.working_dir = Some(PathBuf::from(&args[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--working-dir=") => {
|
||||||
|
opts.working_dir = Some(PathBuf::from(s.trim_start_matches("--working-dir=")));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
opts
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for InitOptions {
|
impl Default for InitOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -84,10 +401,16 @@ pub fn parse_init_args(args: &[String]) -> Option<InitOptions> {
|
|||||||
|
|
||||||
/// Run the fire-and-forget setup.
|
/// Run the fire-and-forget setup.
|
||||||
pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use crate::service::{self, InitSystem, ServiceOptions};
|
||||||
|
|
||||||
eprintln!("[telemt] Fire-and-forget setup");
|
eprintln!("[telemt] Fire-and-forget setup");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
// 1. Generate or validate secret
|
// 1. Detect init system
|
||||||
|
let init_system = service::detect_init_system();
|
||||||
|
eprintln!("[+] Detected init system: {}", init_system);
|
||||||
|
|
||||||
|
// 2. Generate or validate secret
|
||||||
let secret = match opts.secret {
|
let secret = match opts.secret {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
|
if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
@@ -104,72 +427,126 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
eprintln!("[+] Port: {}", opts.port);
|
eprintln!("[+] Port: {}", opts.port);
|
||||||
eprintln!("[+] Domain: {}", opts.domain);
|
eprintln!("[+] Domain: {}", opts.domain);
|
||||||
|
|
||||||
// 2. Create config directory
|
// 3. Create config directory
|
||||||
fs::create_dir_all(&opts.config_dir)?;
|
fs::create_dir_all(&opts.config_dir)?;
|
||||||
let config_path = opts.config_dir.join("config.toml");
|
let config_path = opts.config_dir.join("config.toml");
|
||||||
|
|
||||||
// 3. Write config
|
// 4. Write config
|
||||||
let config_content = generate_config(&opts.username, &secret, opts.port, &opts.domain);
|
let config_content = generate_config(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
fs::write(&config_path, &config_content)?;
|
fs::write(&config_path, &config_content)?;
|
||||||
eprintln!("[+] Config written to {}", config_path.display());
|
eprintln!("[+] Config written to {}", config_path.display());
|
||||||
|
|
||||||
// 4. Write systemd unit
|
// 5. Generate and write service file
|
||||||
let exe_path =
|
let exe_path = std::env::current_exe()
|
||||||
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("/usr/local/bin/telemt"));
|
.unwrap_or_else(|_| PathBuf::from("/usr/local/bin/telemt"));
|
||||||
|
|
||||||
let unit_path = Path::new("/etc/systemd/system/telemt.service");
|
let service_opts = ServiceOptions {
|
||||||
let unit_content = generate_systemd_unit(&exe_path, &config_path);
|
exe_path: &exe_path,
|
||||||
|
config_path: &config_path,
|
||||||
|
user: None, // Let systemd/init handle user
|
||||||
|
group: None,
|
||||||
|
pid_file: "/var/run/telemt.pid",
|
||||||
|
working_dir: Some("/var/lib/telemt"),
|
||||||
|
description: "Telemt MTProxy - Telegram MTProto Proxy",
|
||||||
|
};
|
||||||
|
|
||||||
match fs::write(unit_path, &unit_content) {
|
let service_path = service::service_file_path(init_system);
|
||||||
|
let service_content = service::generate_service_file(init_system, &service_opts);
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
if let Some(parent) = Path::new(service_path).parent() {
|
||||||
|
let _ = fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::write(service_path, &service_content) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
eprintln!("[+] Systemd unit written to {}", unit_path.display());
|
eprintln!("[+] Service file written to {}", service_path);
|
||||||
|
|
||||||
|
// Make script executable for OpenRC/FreeBSD
|
||||||
|
#[cfg(unix)]
|
||||||
|
if init_system == InitSystem::OpenRC || init_system == InitSystem::FreeBSDRc {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(service_path)?.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(service_path, perms)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[!] Cannot write systemd unit (run as root?): {}", e);
|
eprintln!("[!] Cannot write service file (run as root?): {}", e);
|
||||||
eprintln!("[!] Manual unit file content:");
|
eprintln!("[!] Manual service file content:");
|
||||||
eprintln!("{}", unit_content);
|
eprintln!("{}", service_content);
|
||||||
|
|
||||||
// Still print links and config
|
// Still print links and installation instructions
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("{}", service::installation_instructions(init_system));
|
||||||
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Reload systemd
|
// 6. Install and enable service based on init system
|
||||||
run_cmd("systemctl", &["daemon-reload"]);
|
match init_system {
|
||||||
|
InitSystem::Systemd => {
|
||||||
|
run_cmd("systemctl", &["daemon-reload"]);
|
||||||
|
run_cmd("systemctl", &["enable", "telemt.service"]);
|
||||||
|
eprintln!("[+] Service enabled");
|
||||||
|
|
||||||
// 6. Enable service
|
if !opts.no_start {
|
||||||
run_cmd("systemctl", &["enable", "telemt.service"]);
|
run_cmd("systemctl", &["start", "telemt.service"]);
|
||||||
eprintln!("[+] Service enabled");
|
eprintln!("[+] Service started");
|
||||||
|
|
||||||
// 7. Start service (unless --no-start)
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
if !opts.no_start {
|
let status = Command::new("systemctl")
|
||||||
run_cmd("systemctl", &["start", "telemt.service"]);
|
.args(["is-active", "telemt.service"])
|
||||||
eprintln!("[+] Service started");
|
.output();
|
||||||
|
|
||||||
// Brief delay then check status
|
match status {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
Ok(out) if out.status.success() => {
|
||||||
let status = Command::new("systemctl")
|
eprintln!("[+] Service is running");
|
||||||
.args(["is-active", "telemt.service"])
|
}
|
||||||
.output();
|
_ => {
|
||||||
|
eprintln!("[!] Service may not have started correctly");
|
||||||
match status {
|
eprintln!("[!] Check: journalctl -u telemt.service -n 20");
|
||||||
Ok(out) if out.status.success() => {
|
}
|
||||||
eprintln!("[+] Service is running");
|
}
|
||||||
}
|
} else {
|
||||||
_ => {
|
eprintln!("[+] Service not started (--no-start)");
|
||||||
eprintln!("[!] Service may not have started correctly");
|
eprintln!("[+] Start manually: systemctl start telemt.service");
|
||||||
eprintln!("[!] Check: journalctl -u telemt.service -n 20");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
InitSystem::OpenRC => {
|
||||||
eprintln!("[+] Service not started (--no-start)");
|
run_cmd("rc-update", &["add", "telemt", "default"]);
|
||||||
eprintln!("[+] Start manually: systemctl start telemt.service");
|
eprintln!("[+] Service enabled");
|
||||||
|
|
||||||
|
if !opts.no_start {
|
||||||
|
run_cmd("rc-service", &["telemt", "start"]);
|
||||||
|
eprintln!("[+] Service started");
|
||||||
|
} else {
|
||||||
|
eprintln!("[+] Service not started (--no-start)");
|
||||||
|
eprintln!("[+] Start manually: rc-service telemt start");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InitSystem::FreeBSDRc => {
|
||||||
|
run_cmd("sysrc", &["telemt_enable=YES"]);
|
||||||
|
eprintln!("[+] Service enabled");
|
||||||
|
|
||||||
|
if !opts.no_start {
|
||||||
|
run_cmd("service", &["telemt", "start"]);
|
||||||
|
eprintln!("[+] Service started");
|
||||||
|
} else {
|
||||||
|
eprintln!("[+] Service not started (--no-start)");
|
||||||
|
eprintln!("[+] Start manually: service telemt start");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InitSystem::Unknown => {
|
||||||
|
eprintln!("[!] Unknown init system - service file written but not installed");
|
||||||
|
eprintln!("[!] You may need to install it manually");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
// 8. Print links
|
// 7. Print links
|
||||||
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -264,35 +641,6 @@ weight = 10
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_systemd_unit(exe_path: &Path, config_path: &Path) -> String {
|
|
||||||
format!(
|
|
||||||
r#"[Unit]
|
|
||||||
Description=Telemt MTProxy
|
|
||||||
Documentation=https://github.com/telemt/telemt
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart={exe} {config}
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
LimitNOFILE=65535
|
|
||||||
# Security hardening
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=/etc/telemt
|
|
||||||
PrivateTmp=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
"#,
|
|
||||||
exe = exe_path.display(),
|
|
||||||
config = config_path.display(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_cmd(cmd: &str, args: &[&str]) {
|
fn run_cmd(cmd: &str, args: &[&str]) {
|
||||||
match Command::new(cmd).args(args).output() {
|
match Command::new(cmd).args(args).output() {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_FRAMES: usize = 32;
|
|||||||
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_BYTES: usize = 128 * 1024;
|
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_BYTES: usize = 128 * 1024;
|
||||||
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_DELAY_US: u64 = 500;
|
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_DELAY_US: u64 = 500;
|
||||||
const DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE: bool = true;
|
const DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE: bool = true;
|
||||||
|
const DEFAULT_ME_QUOTA_SOFT_OVERSHOOT_BYTES: u64 = 64 * 1024;
|
||||||
|
const DEFAULT_ME_D2C_FRAME_BUF_SHRINK_THRESHOLD_BYTES: usize = 256 * 1024;
|
||||||
const DEFAULT_DIRECT_RELAY_COPY_BUF_C2S_BYTES: usize = 64 * 1024;
|
const DEFAULT_DIRECT_RELAY_COPY_BUF_C2S_BYTES: usize = 64 * 1024;
|
||||||
const DEFAULT_DIRECT_RELAY_COPY_BUF_S2C_BYTES: usize = 256 * 1024;
|
const DEFAULT_DIRECT_RELAY_COPY_BUF_S2C_BYTES: usize = 256 * 1024;
|
||||||
const DEFAULT_ME_WRITER_PICK_SAMPLE_SIZE: u8 = 3;
|
const DEFAULT_ME_WRITER_PICK_SAMPLE_SIZE: u8 = 3;
|
||||||
@@ -387,6 +389,14 @@ pub(crate) fn default_me_d2c_ack_flush_immediate() -> bool {
|
|||||||
DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE
|
DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_quota_soft_overshoot_bytes() -> u64 {
|
||||||
|
DEFAULT_ME_QUOTA_SOFT_OVERSHOOT_BYTES
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_d2c_frame_buf_shrink_threshold_bytes() -> usize {
|
||||||
|
DEFAULT_ME_D2C_FRAME_BUF_SHRINK_THRESHOLD_BYTES
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_direct_relay_copy_buf_c2s_bytes() -> usize {
|
pub(crate) fn default_direct_relay_copy_buf_c2s_bytes() -> usize {
|
||||||
DEFAULT_DIRECT_RELAY_COPY_BUF_C2S_BYTES
|
DEFAULT_DIRECT_RELAY_COPY_BUF_C2S_BYTES
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ pub struct HotFields {
|
|||||||
pub me_d2c_flush_batch_max_bytes: usize,
|
pub me_d2c_flush_batch_max_bytes: usize,
|
||||||
pub me_d2c_flush_batch_max_delay_us: u64,
|
pub me_d2c_flush_batch_max_delay_us: u64,
|
||||||
pub me_d2c_ack_flush_immediate: bool,
|
pub me_d2c_ack_flush_immediate: bool,
|
||||||
|
pub me_quota_soft_overshoot_bytes: u64,
|
||||||
|
pub me_d2c_frame_buf_shrink_threshold_bytes: usize,
|
||||||
pub direct_relay_copy_buf_c2s_bytes: usize,
|
pub direct_relay_copy_buf_c2s_bytes: usize,
|
||||||
pub direct_relay_copy_buf_s2c_bytes: usize,
|
pub direct_relay_copy_buf_s2c_bytes: usize,
|
||||||
pub me_health_interval_ms_unhealthy: u64,
|
pub me_health_interval_ms_unhealthy: u64,
|
||||||
@@ -225,6 +227,8 @@ impl HotFields {
|
|||||||
me_d2c_flush_batch_max_bytes: cfg.general.me_d2c_flush_batch_max_bytes,
|
me_d2c_flush_batch_max_bytes: cfg.general.me_d2c_flush_batch_max_bytes,
|
||||||
me_d2c_flush_batch_max_delay_us: cfg.general.me_d2c_flush_batch_max_delay_us,
|
me_d2c_flush_batch_max_delay_us: cfg.general.me_d2c_flush_batch_max_delay_us,
|
||||||
me_d2c_ack_flush_immediate: cfg.general.me_d2c_ack_flush_immediate,
|
me_d2c_ack_flush_immediate: cfg.general.me_d2c_ack_flush_immediate,
|
||||||
|
me_quota_soft_overshoot_bytes: cfg.general.me_quota_soft_overshoot_bytes,
|
||||||
|
me_d2c_frame_buf_shrink_threshold_bytes: cfg.general.me_d2c_frame_buf_shrink_threshold_bytes,
|
||||||
direct_relay_copy_buf_c2s_bytes: cfg.general.direct_relay_copy_buf_c2s_bytes,
|
direct_relay_copy_buf_c2s_bytes: cfg.general.direct_relay_copy_buf_c2s_bytes,
|
||||||
direct_relay_copy_buf_s2c_bytes: cfg.general.direct_relay_copy_buf_s2c_bytes,
|
direct_relay_copy_buf_s2c_bytes: cfg.general.direct_relay_copy_buf_s2c_bytes,
|
||||||
me_health_interval_ms_unhealthy: cfg.general.me_health_interval_ms_unhealthy,
|
me_health_interval_ms_unhealthy: cfg.general.me_health_interval_ms_unhealthy,
|
||||||
@@ -511,6 +515,9 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
|||||||
cfg.general.me_d2c_flush_batch_max_bytes = new.general.me_d2c_flush_batch_max_bytes;
|
cfg.general.me_d2c_flush_batch_max_bytes = new.general.me_d2c_flush_batch_max_bytes;
|
||||||
cfg.general.me_d2c_flush_batch_max_delay_us = new.general.me_d2c_flush_batch_max_delay_us;
|
cfg.general.me_d2c_flush_batch_max_delay_us = new.general.me_d2c_flush_batch_max_delay_us;
|
||||||
cfg.general.me_d2c_ack_flush_immediate = new.general.me_d2c_ack_flush_immediate;
|
cfg.general.me_d2c_ack_flush_immediate = new.general.me_d2c_ack_flush_immediate;
|
||||||
|
cfg.general.me_quota_soft_overshoot_bytes = new.general.me_quota_soft_overshoot_bytes;
|
||||||
|
cfg.general.me_d2c_frame_buf_shrink_threshold_bytes =
|
||||||
|
new.general.me_d2c_frame_buf_shrink_threshold_bytes;
|
||||||
cfg.general.direct_relay_copy_buf_c2s_bytes = new.general.direct_relay_copy_buf_c2s_bytes;
|
cfg.general.direct_relay_copy_buf_c2s_bytes = new.general.direct_relay_copy_buf_c2s_bytes;
|
||||||
cfg.general.direct_relay_copy_buf_s2c_bytes = new.general.direct_relay_copy_buf_s2c_bytes;
|
cfg.general.direct_relay_copy_buf_s2c_bytes = new.general.direct_relay_copy_buf_s2c_bytes;
|
||||||
cfg.general.me_health_interval_ms_unhealthy = new.general.me_health_interval_ms_unhealthy;
|
cfg.general.me_health_interval_ms_unhealthy = new.general.me_health_interval_ms_unhealthy;
|
||||||
@@ -1030,15 +1037,20 @@ fn log_changes(
|
|||||||
|| old_hot.me_d2c_flush_batch_max_bytes != new_hot.me_d2c_flush_batch_max_bytes
|
|| old_hot.me_d2c_flush_batch_max_bytes != new_hot.me_d2c_flush_batch_max_bytes
|
||||||
|| old_hot.me_d2c_flush_batch_max_delay_us != new_hot.me_d2c_flush_batch_max_delay_us
|
|| old_hot.me_d2c_flush_batch_max_delay_us != new_hot.me_d2c_flush_batch_max_delay_us
|
||||||
|| old_hot.me_d2c_ack_flush_immediate != new_hot.me_d2c_ack_flush_immediate
|
|| old_hot.me_d2c_ack_flush_immediate != new_hot.me_d2c_ack_flush_immediate
|
||||||
|
|| old_hot.me_quota_soft_overshoot_bytes != new_hot.me_quota_soft_overshoot_bytes
|
||||||
|
|| old_hot.me_d2c_frame_buf_shrink_threshold_bytes
|
||||||
|
!= new_hot.me_d2c_frame_buf_shrink_threshold_bytes
|
||||||
|| old_hot.direct_relay_copy_buf_c2s_bytes != new_hot.direct_relay_copy_buf_c2s_bytes
|
|| old_hot.direct_relay_copy_buf_c2s_bytes != new_hot.direct_relay_copy_buf_c2s_bytes
|
||||||
|| old_hot.direct_relay_copy_buf_s2c_bytes != new_hot.direct_relay_copy_buf_s2c_bytes
|
|| old_hot.direct_relay_copy_buf_s2c_bytes != new_hot.direct_relay_copy_buf_s2c_bytes
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"config reload: relay_tuning: me_d2c_frames={} me_d2c_bytes={} me_d2c_delay_us={} me_ack_flush_immediate={} direct_buf_c2s={} direct_buf_s2c={}",
|
"config reload: relay_tuning: me_d2c_frames={} me_d2c_bytes={} me_d2c_delay_us={} me_ack_flush_immediate={} me_quota_soft_overshoot_bytes={} me_d2c_frame_buf_shrink_threshold_bytes={} direct_buf_c2s={} direct_buf_s2c={}",
|
||||||
new_hot.me_d2c_flush_batch_max_frames,
|
new_hot.me_d2c_flush_batch_max_frames,
|
||||||
new_hot.me_d2c_flush_batch_max_bytes,
|
new_hot.me_d2c_flush_batch_max_bytes,
|
||||||
new_hot.me_d2c_flush_batch_max_delay_us,
|
new_hot.me_d2c_flush_batch_max_delay_us,
|
||||||
new_hot.me_d2c_ack_flush_immediate,
|
new_hot.me_d2c_ack_flush_immediate,
|
||||||
|
new_hot.me_quota_soft_overshoot_bytes,
|
||||||
|
new_hot.me_d2c_frame_buf_shrink_threshold_bytes,
|
||||||
new_hot.direct_relay_copy_buf_c2s_bytes,
|
new_hot.direct_relay_copy_buf_c2s_bytes,
|
||||||
new_hot.direct_relay_copy_buf_s2c_bytes,
|
new_hot.direct_relay_copy_buf_s2c_bytes,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -533,6 +533,19 @@ impl ProxyConfig {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.general.me_quota_soft_overshoot_bytes > 16 * 1024 * 1024 {
|
||||||
|
return Err(ProxyError::Config(
|
||||||
|
"general.me_quota_soft_overshoot_bytes must be within [0, 16777216]".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(4096..=16 * 1024 * 1024).contains(&config.general.me_d2c_frame_buf_shrink_threshold_bytes) {
|
||||||
|
return Err(ProxyError::Config(
|
||||||
|
"general.me_d2c_frame_buf_shrink_threshold_bytes must be within [4096, 16777216]"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if !(4096..=1024 * 1024).contains(&config.general.direct_relay_copy_buf_c2s_bytes) {
|
if !(4096..=1024 * 1024).contains(&config.general.direct_relay_copy_buf_c2s_bytes) {
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"general.direct_relay_copy_buf_c2s_bytes must be within [4096, 1048576]"
|
"general.direct_relay_copy_buf_c2s_bytes must be within [4096, 1048576]"
|
||||||
|
|||||||
+11
-1
@@ -468,7 +468,7 @@ pub struct GeneralConfig {
|
|||||||
pub me_c2me_send_timeout_ms: u64,
|
pub me_c2me_send_timeout_ms: u64,
|
||||||
|
|
||||||
/// Bounded wait in milliseconds for routing ME DATA to per-connection queue.
|
/// Bounded wait in milliseconds for routing ME DATA to per-connection queue.
|
||||||
/// `0` keeps legacy no-wait behavior.
|
/// `0` keeps non-blocking routing; values >0 enable bounded wait for compatibility.
|
||||||
#[serde(default = "default_me_reader_route_data_wait_ms")]
|
#[serde(default = "default_me_reader_route_data_wait_ms")]
|
||||||
pub me_reader_route_data_wait_ms: u64,
|
pub me_reader_route_data_wait_ms: u64,
|
||||||
|
|
||||||
@@ -489,6 +489,14 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_me_d2c_ack_flush_immediate")]
|
#[serde(default = "default_me_d2c_ack_flush_immediate")]
|
||||||
pub me_d2c_ack_flush_immediate: bool,
|
pub me_d2c_ack_flush_immediate: bool,
|
||||||
|
|
||||||
|
/// Additional bytes above strict per-user quota allowed in hot-path soft mode.
|
||||||
|
#[serde(default = "default_me_quota_soft_overshoot_bytes")]
|
||||||
|
pub me_quota_soft_overshoot_bytes: u64,
|
||||||
|
|
||||||
|
/// Shrink threshold for reusable ME->Client frame assembly buffer.
|
||||||
|
#[serde(default = "default_me_d2c_frame_buf_shrink_threshold_bytes")]
|
||||||
|
pub me_d2c_frame_buf_shrink_threshold_bytes: usize,
|
||||||
|
|
||||||
/// Copy buffer size for client->DC direction in direct relay.
|
/// Copy buffer size for client->DC direction in direct relay.
|
||||||
#[serde(default = "default_direct_relay_copy_buf_c2s_bytes")]
|
#[serde(default = "default_direct_relay_copy_buf_c2s_bytes")]
|
||||||
pub direct_relay_copy_buf_c2s_bytes: usize,
|
pub direct_relay_copy_buf_c2s_bytes: usize,
|
||||||
@@ -945,6 +953,8 @@ impl Default for GeneralConfig {
|
|||||||
me_d2c_flush_batch_max_bytes: default_me_d2c_flush_batch_max_bytes(),
|
me_d2c_flush_batch_max_bytes: default_me_d2c_flush_batch_max_bytes(),
|
||||||
me_d2c_flush_batch_max_delay_us: default_me_d2c_flush_batch_max_delay_us(),
|
me_d2c_flush_batch_max_delay_us: default_me_d2c_flush_batch_max_delay_us(),
|
||||||
me_d2c_ack_flush_immediate: default_me_d2c_ack_flush_immediate(),
|
me_d2c_ack_flush_immediate: default_me_d2c_ack_flush_immediate(),
|
||||||
|
me_quota_soft_overshoot_bytes: default_me_quota_soft_overshoot_bytes(),
|
||||||
|
me_d2c_frame_buf_shrink_threshold_bytes: default_me_d2c_frame_buf_shrink_threshold_bytes(),
|
||||||
direct_relay_copy_buf_c2s_bytes: default_direct_relay_copy_buf_c2s_bytes(),
|
direct_relay_copy_buf_c2s_bytes: default_direct_relay_copy_buf_c2s_bytes(),
|
||||||
direct_relay_copy_buf_s2c_bytes: default_direct_relay_copy_buf_s2c_bytes(),
|
direct_relay_copy_buf_s2c_bytes: default_direct_relay_copy_buf_s2c_bytes(),
|
||||||
me_warmup_stagger_enabled: default_true(),
|
me_warmup_stagger_enabled: default_true(),
|
||||||
|
|||||||
@@ -0,0 +1,533 @@
|
|||||||
|
//! Unix daemon support for telemt.
|
||||||
|
//!
|
||||||
|
//! Provides classic Unix daemonization (double-fork), PID file management,
|
||||||
|
//! and privilege dropping for running telemt as a background service.
|
||||||
|
|
||||||
|
use std::fs::{self, File, OpenOptions};
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use nix::fcntl::{Flock, FlockArg};
|
||||||
|
use nix::unistd::{self, ForkResult, Gid, Pid, Uid, chdir, close, fork, getpid, setsid};
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
/// Default PID file location.
|
||||||
|
pub const DEFAULT_PID_FILE: &str = "/var/run/telemt.pid";
|
||||||
|
|
||||||
|
/// Daemon configuration options parsed from CLI.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct DaemonOptions {
|
||||||
|
/// Run as daemon (fork to background).
|
||||||
|
pub daemonize: bool,
|
||||||
|
/// Path to PID file.
|
||||||
|
pub pid_file: Option<PathBuf>,
|
||||||
|
/// User to run as after binding sockets.
|
||||||
|
pub user: Option<String>,
|
||||||
|
/// Group to run as after binding sockets.
|
||||||
|
pub group: Option<String>,
|
||||||
|
/// Working directory for the daemon.
|
||||||
|
pub working_dir: Option<PathBuf>,
|
||||||
|
/// Explicit foreground mode (for systemd Type=simple).
|
||||||
|
pub foreground: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DaemonOptions {
|
||||||
|
/// Returns the effective PID file path.
|
||||||
|
pub fn pid_file_path(&self) -> &Path {
|
||||||
|
self.pid_file
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(Path::new(DEFAULT_PID_FILE))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if we should actually daemonize.
|
||||||
|
/// Foreground flag takes precedence.
|
||||||
|
pub fn should_daemonize(&self) -> bool {
|
||||||
|
self.daemonize && !self.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error types for daemon operations.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum DaemonError {
|
||||||
|
#[error("fork failed: {0}")]
|
||||||
|
ForkFailed(#[source] nix::Error),
|
||||||
|
|
||||||
|
#[error("setsid failed: {0}")]
|
||||||
|
SetsidFailed(#[source] nix::Error),
|
||||||
|
|
||||||
|
#[error("chdir failed: {0}")]
|
||||||
|
ChdirFailed(#[source] nix::Error),
|
||||||
|
|
||||||
|
#[error("failed to open /dev/null: {0}")]
|
||||||
|
DevNullFailed(#[source] io::Error),
|
||||||
|
|
||||||
|
#[error("failed to redirect stdio: {0}")]
|
||||||
|
RedirectFailed(#[source] nix::Error),
|
||||||
|
|
||||||
|
#[error("PID file error: {0}")]
|
||||||
|
PidFile(String),
|
||||||
|
|
||||||
|
#[error("another instance is already running (pid {0})")]
|
||||||
|
AlreadyRunning(i32),
|
||||||
|
|
||||||
|
#[error("user '{0}' not found")]
|
||||||
|
UserNotFound(String),
|
||||||
|
|
||||||
|
#[error("group '{0}' not found")]
|
||||||
|
GroupNotFound(String),
|
||||||
|
|
||||||
|
#[error("failed to set uid/gid: {0}")]
|
||||||
|
PrivilegeDrop(#[source] nix::Error),
|
||||||
|
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a successful daemonize() call.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DaemonizeResult {
|
||||||
|
/// We are the parent process and should exit.
|
||||||
|
Parent,
|
||||||
|
/// We are the daemon child process and should continue.
|
||||||
|
Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs classic Unix double-fork daemonization.
|
||||||
|
///
|
||||||
|
/// This detaches the process from the controlling terminal:
|
||||||
|
/// 1. First fork - parent exits, child continues
|
||||||
|
/// 2. setsid() - become session leader
|
||||||
|
/// 3. Second fork - ensure we can never acquire a controlling terminal
|
||||||
|
/// 4. chdir("/") - don't hold any directory open
|
||||||
|
/// 5. Redirect stdin/stdout/stderr to /dev/null
|
||||||
|
///
|
||||||
|
/// Returns `DaemonizeResult::Parent` in the original parent (which should exit),
|
||||||
|
/// or `DaemonizeResult::Child` in the final daemon child.
|
||||||
|
pub fn daemonize(working_dir: Option<&Path>) -> Result<DaemonizeResult, DaemonError> {
|
||||||
|
// First fork
|
||||||
|
match unsafe { fork() } {
|
||||||
|
Ok(ForkResult::Parent { .. }) => {
|
||||||
|
// Parent exits
|
||||||
|
return Ok(DaemonizeResult::Parent);
|
||||||
|
}
|
||||||
|
Ok(ForkResult::Child) => {
|
||||||
|
// Child continues
|
||||||
|
}
|
||||||
|
Err(e) => return Err(DaemonError::ForkFailed(e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new session, become session leader
|
||||||
|
setsid().map_err(DaemonError::SetsidFailed)?;
|
||||||
|
|
||||||
|
// Second fork to ensure we can never acquire a controlling terminal
|
||||||
|
match unsafe { fork() } {
|
||||||
|
Ok(ForkResult::Parent { .. }) => {
|
||||||
|
// Intermediate parent exits
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Ok(ForkResult::Child) => {
|
||||||
|
// Final daemon child continues
|
||||||
|
}
|
||||||
|
Err(e) => return Err(DaemonError::ForkFailed(e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change working directory
|
||||||
|
let target_dir = working_dir.unwrap_or(Path::new("/"));
|
||||||
|
chdir(target_dir).map_err(DaemonError::ChdirFailed)?;
|
||||||
|
|
||||||
|
// Redirect stdin, stdout, stderr to /dev/null
|
||||||
|
redirect_stdio_to_devnull()?;
|
||||||
|
|
||||||
|
Ok(DaemonizeResult::Child)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redirects stdin, stdout, and stderr to /dev/null.
|
||||||
|
fn redirect_stdio_to_devnull() -> Result<(), DaemonError> {
|
||||||
|
let devnull = File::options()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open("/dev/null")
|
||||||
|
.map_err(DaemonError::DevNullFailed)?;
|
||||||
|
|
||||||
|
let devnull_fd = std::os::unix::io::AsRawFd::as_raw_fd(&devnull);
|
||||||
|
|
||||||
|
// Use libc::dup2 directly for redirecting standard file descriptors
|
||||||
|
// nix 0.31's dup2 requires OwnedFd which doesn't work well with stdio fds
|
||||||
|
unsafe {
|
||||||
|
// Redirect stdin (fd 0)
|
||||||
|
if libc::dup2(devnull_fd, 0) < 0 {
|
||||||
|
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
||||||
|
}
|
||||||
|
// Redirect stdout (fd 1)
|
||||||
|
if libc::dup2(devnull_fd, 1) < 0 {
|
||||||
|
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
||||||
|
}
|
||||||
|
// Redirect stderr (fd 2)
|
||||||
|
if libc::dup2(devnull_fd, 2) < 0 {
|
||||||
|
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close original devnull fd if it's not one of the standard fds
|
||||||
|
if devnull_fd > 2 {
|
||||||
|
let _ = close(devnull_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PID file manager with flock-based locking.
|
||||||
|
pub struct PidFile {
|
||||||
|
path: PathBuf,
|
||||||
|
file: Option<File>,
|
||||||
|
locked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PidFile {
|
||||||
|
/// Creates a new PID file manager for the given path.
|
||||||
|
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||||
|
Self {
|
||||||
|
path: path.as_ref().to_path_buf(),
|
||||||
|
file: None,
|
||||||
|
locked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if another instance is already running.
|
||||||
|
///
|
||||||
|
/// Returns the PID of the running instance if one exists.
|
||||||
|
pub fn check_running(&self) -> Result<Option<i32>, DaemonError> {
|
||||||
|
if !self.path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read existing PID
|
||||||
|
let mut contents = String::new();
|
||||||
|
File::open(&self.path)
|
||||||
|
.and_then(|mut f| f.read_to_string(&mut contents))
|
||||||
|
.map_err(|e| DaemonError::PidFile(format!("cannot read {}: {}", self.path.display(), e)))?;
|
||||||
|
|
||||||
|
let pid: i32 = contents
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| DaemonError::PidFile(format!("invalid PID in {}", self.path.display())))?;
|
||||||
|
|
||||||
|
// Check if process is still running
|
||||||
|
if is_process_running(pid) {
|
||||||
|
Ok(Some(pid))
|
||||||
|
} else {
|
||||||
|
// Stale PID file
|
||||||
|
debug!(pid, path = %self.path.display(), "Removing stale PID file");
|
||||||
|
let _ = fs::remove_file(&self.path);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acquires the PID file lock and writes the current PID.
|
||||||
|
///
|
||||||
|
/// Fails if another instance is already running.
|
||||||
|
pub fn acquire(&mut self) -> Result<(), DaemonError> {
|
||||||
|
// Check for running instance first
|
||||||
|
if let Some(pid) = self.check_running()? {
|
||||||
|
return Err(DaemonError::AlreadyRunning(pid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
if let Some(parent) = self.path.parent() {
|
||||||
|
if !parent.exists() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| {
|
||||||
|
DaemonError::PidFile(format!(
|
||||||
|
"cannot create directory {}: {}",
|
||||||
|
parent.display(),
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open/create PID file with exclusive lock
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.mode(0o644)
|
||||||
|
.open(&self.path)
|
||||||
|
.map_err(|e| {
|
||||||
|
DaemonError::PidFile(format!("cannot open {}: {}", self.path.display(), e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Try to acquire exclusive lock (non-blocking)
|
||||||
|
let flock = Flock::lock(file, FlockArg::LockExclusiveNonblock).map_err(|(_, errno)| {
|
||||||
|
// Check if another instance grabbed the lock
|
||||||
|
if let Some(pid) = self.check_running().ok().flatten() {
|
||||||
|
DaemonError::AlreadyRunning(pid)
|
||||||
|
} else {
|
||||||
|
DaemonError::PidFile(format!("cannot lock {}: {}", self.path.display(), errno))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Write our PID
|
||||||
|
let pid = getpid();
|
||||||
|
let mut file = flock.unlock().map_err(|(_, errno)| {
|
||||||
|
DaemonError::PidFile(format!("unlock failed: {}", errno))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
writeln!(file, "{}", pid).map_err(|e| {
|
||||||
|
DaemonError::PidFile(format!("cannot write PID to {}: {}", self.path.display(), e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Re-acquire lock and keep it
|
||||||
|
let flock = Flock::lock(file, FlockArg::LockExclusiveNonblock).map_err(|(_, errno)| {
|
||||||
|
DaemonError::PidFile(format!("cannot re-lock {}: {}", self.path.display(), errno))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.file = Some(flock.unlock().map_err(|(_, errno)| {
|
||||||
|
DaemonError::PidFile(format!("unlock for storage failed: {}", errno))
|
||||||
|
})?);
|
||||||
|
self.locked = true;
|
||||||
|
|
||||||
|
info!(pid = pid.as_raw(), path = %self.path.display(), "PID file created");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases the PID file lock and removes the file.
|
||||||
|
pub fn release(&mut self) -> Result<(), DaemonError> {
|
||||||
|
if let Some(file) = self.file.take() {
|
||||||
|
drop(file);
|
||||||
|
}
|
||||||
|
self.locked = false;
|
||||||
|
|
||||||
|
if self.path.exists() {
|
||||||
|
fs::remove_file(&self.path).map_err(|e| {
|
||||||
|
DaemonError::PidFile(format!("cannot remove {}: {}", self.path.display(), e))
|
||||||
|
})?;
|
||||||
|
debug!(path = %self.path.display(), "PID file removed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to this PID file.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PidFile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.locked {
|
||||||
|
if let Err(e) = self.release() {
|
||||||
|
warn!(error = %e, "Failed to clean up PID file on drop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a process with the given PID is running.
|
||||||
|
fn is_process_running(pid: i32) -> bool {
|
||||||
|
// kill(pid, 0) checks if process exists without sending a signal
|
||||||
|
nix::sys::signal::kill(Pid::from_raw(pid), None).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops privileges to the specified user and group.
|
||||||
|
///
|
||||||
|
/// This should be called after binding privileged ports but before
|
||||||
|
/// entering the main event loop.
|
||||||
|
pub fn drop_privileges(user: Option<&str>, group: Option<&str>) -> Result<(), DaemonError> {
|
||||||
|
// Look up group first (need to do this while still root)
|
||||||
|
let target_gid = if let Some(group_name) = group {
|
||||||
|
Some(lookup_group(group_name)?)
|
||||||
|
} else if let Some(user_name) = user {
|
||||||
|
// If no group specified but user is, use user's primary group
|
||||||
|
Some(lookup_user_primary_gid(user_name)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look up user
|
||||||
|
let target_uid = if let Some(user_name) = user {
|
||||||
|
Some(lookup_user(user_name)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drop privileges: set GID first, then UID
|
||||||
|
// (Setting UID first would prevent us from setting GID)
|
||||||
|
if let Some(gid) = target_gid {
|
||||||
|
unistd::setgid(gid).map_err(DaemonError::PrivilegeDrop)?;
|
||||||
|
// Also set supplementary groups to just this one
|
||||||
|
unistd::setgroups(&[gid]).map_err(DaemonError::PrivilegeDrop)?;
|
||||||
|
info!(gid = gid.as_raw(), "Dropped group privileges");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(uid) = target_uid {
|
||||||
|
unistd::setuid(uid).map_err(DaemonError::PrivilegeDrop)?;
|
||||||
|
info!(uid = uid.as_raw(), "Dropped user privileges");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up a user by name and returns their UID.
|
||||||
|
fn lookup_user(name: &str) -> Result<Uid, DaemonError> {
|
||||||
|
// Use libc getpwnam
|
||||||
|
let c_name = std::ffi::CString::new(name).map_err(|_| DaemonError::UserNotFound(name.to_string()))?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let pwd = libc::getpwnam(c_name.as_ptr());
|
||||||
|
if pwd.is_null() {
|
||||||
|
Err(DaemonError::UserNotFound(name.to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(Uid::from_raw((*pwd).pw_uid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up a user's primary GID by username.
|
||||||
|
fn lookup_user_primary_gid(name: &str) -> Result<Gid, DaemonError> {
|
||||||
|
let c_name = std::ffi::CString::new(name).map_err(|_| DaemonError::UserNotFound(name.to_string()))?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let pwd = libc::getpwnam(c_name.as_ptr());
|
||||||
|
if pwd.is_null() {
|
||||||
|
Err(DaemonError::UserNotFound(name.to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(Gid::from_raw((*pwd).pw_gid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up a group by name and returns its GID.
|
||||||
|
fn lookup_group(name: &str) -> Result<Gid, DaemonError> {
|
||||||
|
let c_name = std::ffi::CString::new(name).map_err(|_| DaemonError::GroupNotFound(name.to_string()))?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let grp = libc::getgrnam(c_name.as_ptr());
|
||||||
|
if grp.is_null() {
|
||||||
|
Err(DaemonError::GroupNotFound(name.to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(Gid::from_raw((*grp).gr_gid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads PID from a PID file.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn read_pid_file<P: AsRef<Path>>(path: P) -> Result<i32, DaemonError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let mut contents = String::new();
|
||||||
|
File::open(path)
|
||||||
|
.and_then(|mut f| f.read_to_string(&mut contents))
|
||||||
|
.map_err(|e| DaemonError::PidFile(format!("cannot read {}: {}", path.display(), e)))?;
|
||||||
|
|
||||||
|
contents
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| DaemonError::PidFile(format!("invalid PID in {}", path.display())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a signal to the process specified in a PID file.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn signal_pid_file<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
signal: nix::sys::signal::Signal,
|
||||||
|
) -> Result<(), DaemonError> {
|
||||||
|
let pid = read_pid_file(&path)?;
|
||||||
|
|
||||||
|
if !is_process_running(pid) {
|
||||||
|
return Err(DaemonError::PidFile(format!(
|
||||||
|
"process {} from {} is not running",
|
||||||
|
pid,
|
||||||
|
path.as_ref().display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
nix::sys::signal::kill(Pid::from_raw(pid), signal).map_err(|e| {
|
||||||
|
DaemonError::PidFile(format!("cannot signal process {}: {}", pid, e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the status of the daemon based on PID file.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum DaemonStatus {
|
||||||
|
/// Daemon is running with the given PID.
|
||||||
|
Running(i32),
|
||||||
|
/// PID file exists but process is not running.
|
||||||
|
Stale(i32),
|
||||||
|
/// No PID file exists.
|
||||||
|
NotRunning,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the daemon status from a PID file.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn check_status<P: AsRef<Path>>(path: P) -> DaemonStatus {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return DaemonStatus::NotRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
match read_pid_file(path) {
|
||||||
|
Ok(pid) => {
|
||||||
|
if is_process_running(pid) {
|
||||||
|
DaemonStatus::Running(pid)
|
||||||
|
} else {
|
||||||
|
DaemonStatus::Stale(pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => DaemonStatus::NotRunning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_daemon_options_default() {
|
||||||
|
let opts = DaemonOptions::default();
|
||||||
|
assert!(!opts.daemonize);
|
||||||
|
assert!(!opts.should_daemonize());
|
||||||
|
assert_eq!(opts.pid_file_path(), Path::new(DEFAULT_PID_FILE));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_daemon_options_foreground_overrides() {
|
||||||
|
let opts = DaemonOptions {
|
||||||
|
daemonize: true,
|
||||||
|
foreground: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(!opts.should_daemonize());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_check_status_not_running() {
|
||||||
|
let path = "/tmp/telemt_test_nonexistent.pid";
|
||||||
|
assert_eq!(check_status(path), DaemonStatus::NotRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pid_file_basic() {
|
||||||
|
let path = "/tmp/telemt_test_pidfile.pid";
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
|
||||||
|
let mut pf = PidFile::new(path);
|
||||||
|
assert!(pf.check_running().unwrap().is_none());
|
||||||
|
|
||||||
|
pf.acquire().unwrap();
|
||||||
|
assert!(Path::new(path).exists());
|
||||||
|
|
||||||
|
// Read it back
|
||||||
|
let pid = read_pid_file(path).unwrap();
|
||||||
|
assert_eq!(pid, std::process::id() as i32);
|
||||||
|
|
||||||
|
pf.release().unwrap();
|
||||||
|
assert!(!Path::new(path).exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
+291
@@ -0,0 +1,291 @@
|
|||||||
|
//! Logging configuration for telemt.
|
||||||
|
//!
|
||||||
|
//! Supports multiple log destinations:
|
||||||
|
//! - stderr (default, works with systemd journald)
|
||||||
|
//! - syslog (Unix only, for traditional init systems)
|
||||||
|
//! - file (with optional rotation)
|
||||||
|
|
||||||
|
#![allow(dead_code)] // Infrastructure module - used via CLI flags
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
use tracing_subscriber::{EnvFilter, fmt, reload};
|
||||||
|
|
||||||
|
/// Log destination configuration.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub enum LogDestination {
|
||||||
|
/// Log to stderr (default, captured by systemd journald).
|
||||||
|
#[default]
|
||||||
|
Stderr,
|
||||||
|
/// Log to syslog (Unix only).
|
||||||
|
#[cfg(unix)]
|
||||||
|
Syslog,
|
||||||
|
/// Log to a file with optional rotation.
|
||||||
|
File {
|
||||||
|
path: String,
|
||||||
|
/// Rotate daily if true.
|
||||||
|
rotate_daily: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logging options parsed from CLI/config.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct LoggingOptions {
|
||||||
|
/// Where to send logs.
|
||||||
|
pub destination: LogDestination,
|
||||||
|
/// Disable ANSI colors.
|
||||||
|
pub disable_colors: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard that must be held to keep file logging active.
|
||||||
|
/// When dropped, flushes and closes log files.
|
||||||
|
pub struct LoggingGuard {
|
||||||
|
_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoggingGuard {
|
||||||
|
fn new(guard: Option<tracing_appender::non_blocking::WorkerGuard>) -> Self {
|
||||||
|
Self { _guard: guard }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a no-op guard for stderr/syslog logging.
|
||||||
|
pub fn noop() -> Self {
|
||||||
|
Self { _guard: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the tracing subscriber with the specified options.
|
||||||
|
///
|
||||||
|
/// Returns a reload handle for dynamic log level changes and a guard
|
||||||
|
/// that must be kept alive for file logging.
|
||||||
|
pub fn init_logging(
|
||||||
|
opts: &LoggingOptions,
|
||||||
|
initial_filter: &str,
|
||||||
|
) -> (reload::Handle<EnvFilter, impl tracing::Subscriber + Send + Sync>, LoggingGuard) {
|
||||||
|
let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new(initial_filter));
|
||||||
|
|
||||||
|
match &opts.destination {
|
||||||
|
LogDestination::Stderr => {
|
||||||
|
let fmt_layer = fmt::Layer::default()
|
||||||
|
.with_ansi(!opts.disable_colors)
|
||||||
|
.with_target(true);
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter_layer)
|
||||||
|
.with(fmt_layer)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
(filter_handle, LoggingGuard::noop())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
LogDestination::Syslog => {
|
||||||
|
// Use a custom fmt layer that writes to syslog
|
||||||
|
let fmt_layer = fmt::Layer::default()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_target(true)
|
||||||
|
.with_writer(SyslogWriter::new);
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter_layer)
|
||||||
|
.with(fmt_layer)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
(filter_handle, LoggingGuard::noop())
|
||||||
|
}
|
||||||
|
|
||||||
|
LogDestination::File { path, rotate_daily } => {
|
||||||
|
let (non_blocking, guard) = if *rotate_daily {
|
||||||
|
// Extract directory and filename prefix
|
||||||
|
let path = Path::new(path);
|
||||||
|
let dir = path.parent().unwrap_or(Path::new("/var/log"));
|
||||||
|
let prefix = path.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("telemt");
|
||||||
|
|
||||||
|
let file_appender = tracing_appender::rolling::daily(dir, prefix);
|
||||||
|
tracing_appender::non_blocking(file_appender)
|
||||||
|
} else {
|
||||||
|
let file = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(path)
|
||||||
|
.expect("Failed to open log file");
|
||||||
|
tracing_appender::non_blocking(file)
|
||||||
|
};
|
||||||
|
|
||||||
|
let fmt_layer = fmt::Layer::default()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_target(true)
|
||||||
|
.with_writer(non_blocking);
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter_layer)
|
||||||
|
.with(fmt_layer)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
(filter_handle, LoggingGuard::new(Some(guard)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Syslog writer for tracing.
|
||||||
|
#[cfg(unix)]
|
||||||
|
struct SyslogWriter {
|
||||||
|
_private: (),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
impl SyslogWriter {
|
||||||
|
fn new() -> Self {
|
||||||
|
// Open syslog connection on first use
|
||||||
|
static INIT: std::sync::Once = std::sync::Once::new();
|
||||||
|
INIT.call_once(|| {
|
||||||
|
unsafe {
|
||||||
|
// Open syslog with ident "telemt", LOG_PID, LOG_DAEMON facility
|
||||||
|
let ident = b"telemt\0".as_ptr() as *const libc::c_char;
|
||||||
|
libc::openlog(ident, libc::LOG_PID | libc::LOG_NDELAY, libc::LOG_DAEMON);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Self { _private: () }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
impl std::io::Write for SyslogWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
// Convert to C string, stripping newlines
|
||||||
|
let msg = String::from_utf8_lossy(buf);
|
||||||
|
let msg = msg.trim_end();
|
||||||
|
|
||||||
|
if msg.is_empty() {
|
||||||
|
return Ok(buf.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine priority based on log level in the message
|
||||||
|
let priority = if msg.contains(" ERROR ") || msg.contains(" error ") {
|
||||||
|
libc::LOG_ERR
|
||||||
|
} else if msg.contains(" WARN ") || msg.contains(" warn ") {
|
||||||
|
libc::LOG_WARNING
|
||||||
|
} else if msg.contains(" INFO ") || msg.contains(" info ") {
|
||||||
|
libc::LOG_INFO
|
||||||
|
} else if msg.contains(" DEBUG ") || msg.contains(" debug ") {
|
||||||
|
libc::LOG_DEBUG
|
||||||
|
} else {
|
||||||
|
libc::LOG_INFO
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write to syslog
|
||||||
|
let c_msg = std::ffi::CString::new(msg.as_bytes())
|
||||||
|
.unwrap_or_else(|_| std::ffi::CString::new("(invalid utf8)").unwrap());
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
libc::syslog(priority, b"%s\0".as_ptr() as *const libc::c_char, c_msg.as_ptr());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SyslogWriter {
|
||||||
|
type Writer = SyslogWriter;
|
||||||
|
|
||||||
|
fn make_writer(&'a self) -> Self::Writer {
|
||||||
|
SyslogWriter::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse log destination from CLI arguments.
|
||||||
|
pub fn parse_log_destination(args: &[String]) -> LogDestination {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
"--syslog" => {
|
||||||
|
return LogDestination::Syslog;
|
||||||
|
}
|
||||||
|
"--log-file" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
return LogDestination::File {
|
||||||
|
path: args[i].clone(),
|
||||||
|
rotate_daily: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--log-file=") => {
|
||||||
|
return LogDestination::File {
|
||||||
|
path: s.trim_start_matches("--log-file=").to_string(),
|
||||||
|
rotate_daily: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"--log-file-daily" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
return LogDestination::File {
|
||||||
|
path: args[i].clone(),
|
||||||
|
rotate_daily: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--log-file-daily=") => {
|
||||||
|
return LogDestination::File {
|
||||||
|
path: s.trim_start_matches("--log-file-daily=").to_string(),
|
||||||
|
rotate_daily: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
LogDestination::Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_destination_default() {
|
||||||
|
let args: Vec<String> = vec![];
|
||||||
|
assert!(matches!(parse_log_destination(&args), LogDestination::Stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_destination_file() {
|
||||||
|
let args = vec!["--log-file".to_string(), "/var/log/telemt.log".to_string()];
|
||||||
|
match parse_log_destination(&args) {
|
||||||
|
LogDestination::File { path, rotate_daily } => {
|
||||||
|
assert_eq!(path, "/var/log/telemt.log");
|
||||||
|
assert!(!rotate_daily);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected File destination"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_destination_file_daily() {
|
||||||
|
let args = vec!["--log-file-daily=/var/log/telemt".to_string()];
|
||||||
|
match parse_log_destination(&args) {
|
||||||
|
LogDestination::File { path, rotate_daily } => {
|
||||||
|
assert_eq!(path, "/var/log/telemt");
|
||||||
|
assert!(rotate_daily);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected File destination"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_parse_log_destination_syslog() {
|
||||||
|
let args = vec!["--syslog".to_string()];
|
||||||
|
assert!(matches!(parse_log_destination(&args), LogDestination::Syslog));
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
-26
@@ -8,6 +8,7 @@ use tracing::{debug, error, info, warn};
|
|||||||
|
|
||||||
use crate::cli;
|
use crate::cli;
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
|
use crate::logging::LogDestination;
|
||||||
use crate::transport::middle_proxy::{
|
use crate::transport::middle_proxy::{
|
||||||
ProxyConfigData, fetch_proxy_config_with_raw, load_proxy_config_cache, save_proxy_config_cache,
|
ProxyConfigData, fetch_proxy_config_with_raw, load_proxy_config_cache, save_proxy_config_cache,
|
||||||
};
|
};
|
||||||
@@ -25,7 +26,16 @@ pub(crate) fn resolve_runtime_config_path(
|
|||||||
absolute.canonicalize().unwrap_or(absolute)
|
absolute.canonicalize().unwrap_or(absolute)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_cli() -> (String, Option<PathBuf>, bool, Option<String>) {
|
/// Parsed CLI arguments.
|
||||||
|
pub(crate) struct CliArgs {
|
||||||
|
pub config_path: String,
|
||||||
|
pub data_path: Option<PathBuf>,
|
||||||
|
pub silent: bool,
|
||||||
|
pub log_level: Option<String>,
|
||||||
|
pub log_destination: LogDestination,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_cli() -> CliArgs {
|
||||||
let mut config_path = "config.toml".to_string();
|
let mut config_path = "config.toml".to_string();
|
||||||
let mut data_path: Option<PathBuf> = None;
|
let mut data_path: Option<PathBuf> = None;
|
||||||
let mut silent = false;
|
let mut silent = false;
|
||||||
@@ -33,6 +43,9 @@ pub(crate) fn parse_cli() -> (String, Option<PathBuf>, bool, Option<String>) {
|
|||||||
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
|
||||||
|
// Parse log destination
|
||||||
|
let log_destination = crate::logging::parse_log_destination(&args);
|
||||||
|
|
||||||
// Check for --init first (handled before tokio)
|
// Check for --init first (handled before tokio)
|
||||||
if let Some(init_opts) = cli::parse_init_args(&args) {
|
if let Some(init_opts) = cli::parse_init_args(&args) {
|
||||||
if let Err(e) = cli::run_init(init_opts) {
|
if let Err(e) = cli::run_init(init_opts) {
|
||||||
@@ -72,36 +85,35 @@ pub(crate) fn parse_cli() -> (String, Option<PathBuf>, bool, Option<String>) {
|
|||||||
log_level = Some(s.trim_start_matches("--log-level=").to_string());
|
log_level = Some(s.trim_start_matches("--log-level=").to_string());
|
||||||
}
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
eprintln!("Usage: telemt [config.toml] [OPTIONS]");
|
print_help();
|
||||||
eprintln!();
|
|
||||||
eprintln!("Options:");
|
|
||||||
eprintln!(
|
|
||||||
" --data-path <DIR> Set data directory (absolute path; overrides config value)"
|
|
||||||
);
|
|
||||||
eprintln!(" --silent, -s Suppress info logs");
|
|
||||||
eprintln!(" --log-level <LEVEL> debug|verbose|normal|silent");
|
|
||||||
eprintln!(" --help, -h Show this help");
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Setup (fire-and-forget):");
|
|
||||||
eprintln!(
|
|
||||||
" --init Generate config, install systemd service, start"
|
|
||||||
);
|
|
||||||
eprintln!(" --port <PORT> Listen port (default: 443)");
|
|
||||||
eprintln!(
|
|
||||||
" --domain <DOMAIN> TLS domain for masking (default: www.google.com)"
|
|
||||||
);
|
|
||||||
eprintln!(
|
|
||||||
" --secret <HEX> 32-char hex secret (auto-generated if omitted)"
|
|
||||||
);
|
|
||||||
eprintln!(" --user <NAME> Username (default: user)");
|
|
||||||
eprintln!(" --config-dir <DIR> Config directory (default: /etc/telemt)");
|
|
||||||
eprintln!(" --no-start Don't start the service after install");
|
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
"--version" | "-V" => {
|
"--version" | "-V" => {
|
||||||
println!("telemt {}", env!("CARGO_PKG_VERSION"));
|
println!("telemt {}", env!("CARGO_PKG_VERSION"));
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
// Skip daemon-related flags (already parsed)
|
||||||
|
"--daemon" | "-d" | "--foreground" | "-f" => {}
|
||||||
|
s if s.starts_with("--pid-file") => {
|
||||||
|
if !s.contains('=') {
|
||||||
|
i += 1; // skip value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--run-as-user") => {
|
||||||
|
if !s.contains('=') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--run-as-group") => {
|
||||||
|
if !s.contains('=') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--working-dir") => {
|
||||||
|
if !s.contains('=') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
s if !s.starts_with('-') => {
|
s if !s.starts_with('-') => {
|
||||||
config_path = s.to_string();
|
config_path = s.to_string();
|
||||||
}
|
}
|
||||||
@@ -112,7 +124,77 @@ pub(crate) fn parse_cli() -> (String, Option<PathBuf>, bool, Option<String>) {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
(config_path, data_path, silent, log_level)
|
CliArgs {
|
||||||
|
config_path,
|
||||||
|
data_path,
|
||||||
|
silent,
|
||||||
|
log_level,
|
||||||
|
log_destination,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
eprintln!("Usage: telemt [COMMAND] [OPTIONS] [config.toml]");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Commands:");
|
||||||
|
eprintln!(" run Run in foreground (default if no command given)");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
eprintln!(" start Start as background daemon");
|
||||||
|
eprintln!(" stop Stop a running daemon");
|
||||||
|
eprintln!(" reload Reload configuration (send SIGHUP)");
|
||||||
|
eprintln!(" status Check if daemon is running");
|
||||||
|
}
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Options:");
|
||||||
|
eprintln!(" --data-path <DIR> Set data directory (absolute path; overrides config value)");
|
||||||
|
eprintln!(" --silent, -s Suppress info logs");
|
||||||
|
eprintln!(" --log-level <LEVEL> debug|verbose|normal|silent");
|
||||||
|
eprintln!(" --help, -h Show this help");
|
||||||
|
eprintln!(" --version, -V Show version");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Logging options:");
|
||||||
|
eprintln!(" --log-file <PATH> Log to file (default: stderr)");
|
||||||
|
eprintln!(" --log-file-daily <PATH> Log to file with daily rotation");
|
||||||
|
#[cfg(unix)]
|
||||||
|
eprintln!(" --syslog Log to syslog (Unix only)");
|
||||||
|
eprintln!();
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
eprintln!("Daemon options (Unix only):");
|
||||||
|
eprintln!(" --daemon, -d Fork to background (daemonize)");
|
||||||
|
eprintln!(" --foreground, -f Explicit foreground mode (for systemd)");
|
||||||
|
eprintln!(" --pid-file <PATH> PID file path (default: /var/run/telemt.pid)");
|
||||||
|
eprintln!(" --run-as-user <USER> Drop privileges to this user after binding");
|
||||||
|
eprintln!(" --run-as-group <GROUP> Drop privileges to this group after binding");
|
||||||
|
eprintln!(" --working-dir <DIR> Working directory for daemon mode");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
eprintln!("Setup (fire-and-forget):");
|
||||||
|
eprintln!(
|
||||||
|
" --init Generate config, install systemd service, start"
|
||||||
|
);
|
||||||
|
eprintln!(" --port <PORT> Listen port (default: 443)");
|
||||||
|
eprintln!(
|
||||||
|
" --domain <DOMAIN> TLS domain for masking (default: www.google.com)"
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" --secret <HEX> 32-char hex secret (auto-generated if omitted)"
|
||||||
|
);
|
||||||
|
eprintln!(" --user <NAME> Username (default: user)");
|
||||||
|
eprintln!(" --config-dir <DIR> Config directory (default: /etc/telemt)");
|
||||||
|
eprintln!(" --no-start Don't start the service after install");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Examples:");
|
||||||
|
eprintln!(" telemt config.toml Run in foreground");
|
||||||
|
eprintln!(" telemt start config.toml Start as daemon");
|
||||||
|
eprintln!(" telemt start --pid-file /tmp/t.pid Start with custom PID file");
|
||||||
|
eprintln!(" telemt stop Stop daemon");
|
||||||
|
eprintln!(" telemt reload Reload configuration");
|
||||||
|
eprintln!(" telemt status Check daemon status");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
+105
-12
@@ -47,8 +47,56 @@ use crate::transport::UpstreamManager;
|
|||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
use helpers::{parse_cli, resolve_runtime_config_path};
|
use helpers::{parse_cli, resolve_runtime_config_path};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use crate::daemon::{DaemonOptions, PidFile, drop_privileges};
|
||||||
|
|
||||||
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
||||||
|
///
|
||||||
|
/// On Unix, daemon options should be handled before calling this function
|
||||||
|
/// (daemonization must happen before tokio runtime starts).
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub async fn run_with_daemon(
|
||||||
|
daemon_opts: DaemonOptions,
|
||||||
|
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
|
run_inner(daemon_opts).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
||||||
|
///
|
||||||
|
/// This is the main entry point for non-daemon mode or when called as a library.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
// Parse CLI to get daemon options even in simple run() path
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
let daemon_opts = crate::cli::parse_daemon_args(&args);
|
||||||
|
run_inner(daemon_opts).await
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
run_inner().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn run_inner(
|
||||||
|
daemon_opts: DaemonOptions,
|
||||||
|
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
|
// Acquire PID file if daemonizing or if explicitly requested
|
||||||
|
// Keep it alive until shutdown (underscore prefix = intentionally kept for RAII cleanup)
|
||||||
|
let _pid_file = if daemon_opts.daemonize || daemon_opts.pid_file.is_some() {
|
||||||
|
let mut pf = PidFile::new(daemon_opts.pid_file_path());
|
||||||
|
if let Err(e) = pf.acquire() {
|
||||||
|
eprintln!("[telemt] {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Some(pf)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let process_started_at = Instant::now();
|
let process_started_at = Instant::now();
|
||||||
let process_started_at_epoch_secs = SystemTime::now()
|
let process_started_at_epoch_secs = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -61,7 +109,12 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
Some("load and validate config".to_string()),
|
Some("load and validate config".to_string()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (config_path_cli, data_path, cli_silent, cli_log_level) = parse_cli();
|
let cli_args = parse_cli();
|
||||||
|
let config_path_cli = cli_args.config_path;
|
||||||
|
let data_path = cli_args.data_path;
|
||||||
|
let cli_silent = cli_args.silent;
|
||||||
|
let cli_log_level = cli_args.log_level;
|
||||||
|
let log_destination = cli_args.log_destination;
|
||||||
let startup_cwd = match std::env::current_dir() {
|
let startup_cwd = match std::env::current_dir() {
|
||||||
Ok(cwd) => cwd,
|
Ok(cwd) => cwd,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -161,17 +214,43 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Configure color output based on config
|
// Initialize logging based on destination
|
||||||
let fmt_layer = if config.general.disable_colors {
|
let _logging_guard: Option<crate::logging::LoggingGuard>;
|
||||||
fmt::Layer::default().with_ansi(false)
|
match log_destination {
|
||||||
} else {
|
crate::logging::LogDestination::Stderr => {
|
||||||
fmt::Layer::default().with_ansi(true)
|
// Default: log to stderr (works with systemd journald)
|
||||||
};
|
let fmt_layer = if config.general.disable_colors {
|
||||||
|
fmt::Layer::default().with_ansi(false)
|
||||||
|
} else {
|
||||||
|
fmt::Layer::default().with_ansi(true)
|
||||||
|
};
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter_layer)
|
||||||
|
.with(fmt_layer)
|
||||||
|
.init();
|
||||||
|
_logging_guard = None;
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
crate::logging::LogDestination::Syslog => {
|
||||||
|
// Syslog: for OpenRC/FreeBSD
|
||||||
|
let logging_opts = crate::logging::LoggingOptions {
|
||||||
|
destination: log_destination,
|
||||||
|
disable_colors: true,
|
||||||
|
};
|
||||||
|
let (_, guard) = crate::logging::init_logging(&logging_opts, "info");
|
||||||
|
_logging_guard = Some(guard);
|
||||||
|
}
|
||||||
|
crate::logging::LogDestination::File { .. } => {
|
||||||
|
// File logging with optional rotation
|
||||||
|
let logging_opts = crate::logging::LoggingOptions {
|
||||||
|
destination: log_destination,
|
||||||
|
disable_colors: true,
|
||||||
|
};
|
||||||
|
let (_, guard) = crate::logging::init_logging(&logging_opts, "info");
|
||||||
|
_logging_guard = Some(guard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.init();
|
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.complete_component(
|
.complete_component(
|
||||||
COMPONENT_TRACING_INIT,
|
COMPONENT_TRACING_INIT,
|
||||||
@@ -585,6 +664,17 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop privileges after binding sockets (which may require root for port < 1024)
|
||||||
|
if daemon_opts.user.is_some() || daemon_opts.group.is_some() {
|
||||||
|
if let Err(e) = drop_privileges(
|
||||||
|
daemon_opts.user.as_deref(),
|
||||||
|
daemon_opts.group.as_deref(),
|
||||||
|
) {
|
||||||
|
error!(error = %e, "Failed to drop privileges");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runtime_tasks::apply_runtime_log_filter(
|
runtime_tasks::apply_runtime_log_filter(
|
||||||
has_rust_log,
|
has_rust_log,
|
||||||
&effective_log_level,
|
&effective_log_level,
|
||||||
@@ -605,6 +695,9 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
runtime_tasks::mark_runtime_ready(&startup_tracker).await;
|
runtime_tasks::mark_runtime_ready(&startup_tracker).await;
|
||||||
|
|
||||||
|
// Spawn signal handlers for SIGUSR1/SIGUSR2 (non-shutdown signals)
|
||||||
|
shutdown::spawn_signal_handlers(stats.clone(), process_started_at);
|
||||||
|
|
||||||
listeners::spawn_tcp_accept_loops(
|
listeners::spawn_tcp_accept_loops(
|
||||||
listeners,
|
listeners,
|
||||||
config_rx.clone(),
|
config_rx.clone(),
|
||||||
@@ -622,7 +715,7 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
max_connections.clone(),
|
max_connections.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
shutdown::wait_for_shutdown(process_started_at, me_pool).await;
|
shutdown::wait_for_shutdown(process_started_at, me_pool, stats).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+199
-33
@@ -1,45 +1,211 @@
|
|||||||
|
//! Shutdown and signal handling for telemt.
|
||||||
|
//!
|
||||||
|
//! Handles graceful shutdown on various signals:
|
||||||
|
//! - SIGINT (Ctrl+C) / SIGTERM: Graceful shutdown
|
||||||
|
//! - SIGQUIT: Graceful shutdown with stats dump
|
||||||
|
//! - SIGUSR1: Reserved for log rotation (logs acknowledgment)
|
||||||
|
//! - SIGUSR2: Dump runtime status to log
|
||||||
|
//!
|
||||||
|
//! SIGHUP is handled separately in config/hot_reload.rs for config reload.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
|
#[cfg(not(unix))]
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::stats::Stats;
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
|
|
||||||
use super::helpers::{format_uptime, unit_label};
|
use super::helpers::{format_uptime, unit_label};
|
||||||
|
|
||||||
pub(crate) async fn wait_for_shutdown(process_started_at: Instant, me_pool: Option<Arc<MePool>>) {
|
/// Signal that triggered shutdown.
|
||||||
match signal::ctrl_c().await {
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
Ok(()) => {
|
pub enum ShutdownSignal {
|
||||||
let shutdown_started_at = Instant::now();
|
/// SIGINT (Ctrl+C)
|
||||||
info!("Shutting down...");
|
Interrupt,
|
||||||
let uptime_secs = process_started_at.elapsed().as_secs();
|
/// SIGTERM
|
||||||
info!("Uptime: {}", format_uptime(uptime_secs));
|
Terminate,
|
||||||
if let Some(pool) = &me_pool {
|
/// SIGQUIT (with stats dump)
|
||||||
match tokio::time::timeout(
|
Quit,
|
||||||
Duration::from_secs(2),
|
}
|
||||||
pool.shutdown_send_close_conn_all(),
|
|
||||||
)
|
impl std::fmt::Display for ShutdownSignal {
|
||||||
.await
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
{
|
match self {
|
||||||
Ok(total) => {
|
ShutdownSignal::Interrupt => write!(f, "SIGINT"),
|
||||||
info!(
|
ShutdownSignal::Terminate => write!(f, "SIGTERM"),
|
||||||
close_conn_sent = total,
|
ShutdownSignal::Quit => write!(f, "SIGQUIT"),
|
||||||
"ME shutdown: RPC_CLOSE_CONN broadcast completed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
warn!("ME shutdown: RPC_CLOSE_CONN broadcast timed out");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let shutdown_secs = shutdown_started_at.elapsed().as_secs();
|
|
||||||
info!(
|
|
||||||
"Shutdown completed successfully in {} {}.",
|
|
||||||
shutdown_secs,
|
|
||||||
unit_label(shutdown_secs, "second", "seconds")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(e) => error!("Signal error: {}", e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Waits for a shutdown signal and performs graceful shutdown.
|
||||||
|
pub(crate) async fn wait_for_shutdown(
|
||||||
|
process_started_at: Instant,
|
||||||
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
) {
|
||||||
|
let signal = wait_for_shutdown_signal().await;
|
||||||
|
perform_shutdown(signal, process_started_at, me_pool, &stats).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for any shutdown signal (SIGINT, SIGTERM, SIGQUIT).
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn wait_for_shutdown_signal() -> ShutdownSignal {
|
||||||
|
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
|
||||||
|
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
|
||||||
|
let mut sigquit = signal(SignalKind::quit()).expect("Failed to register SIGQUIT handler");
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = sigint.recv() => ShutdownSignal::Interrupt,
|
||||||
|
_ = sigterm.recv() => ShutdownSignal::Terminate,
|
||||||
|
_ = sigquit.recv() => ShutdownSignal::Quit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
async fn wait_for_shutdown_signal() -> ShutdownSignal {
|
||||||
|
signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
|
||||||
|
ShutdownSignal::Interrupt
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs graceful shutdown sequence.
|
||||||
|
async fn perform_shutdown(
|
||||||
|
signal: ShutdownSignal,
|
||||||
|
process_started_at: Instant,
|
||||||
|
me_pool: Option<Arc<MePool>>,
|
||||||
|
stats: &Stats,
|
||||||
|
) {
|
||||||
|
let shutdown_started_at = Instant::now();
|
||||||
|
info!(signal = %signal, "Received shutdown signal");
|
||||||
|
|
||||||
|
// Dump stats if SIGQUIT
|
||||||
|
if signal == ShutdownSignal::Quit {
|
||||||
|
dump_stats(stats, process_started_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Shutting down...");
|
||||||
|
let uptime_secs = process_started_at.elapsed().as_secs();
|
||||||
|
info!("Uptime: {}", format_uptime(uptime_secs));
|
||||||
|
|
||||||
|
// Graceful ME pool shutdown
|
||||||
|
if let Some(pool) = &me_pool {
|
||||||
|
match tokio::time::timeout(Duration::from_secs(2), pool.shutdown_send_close_conn_all()).await
|
||||||
|
{
|
||||||
|
Ok(total) => {
|
||||||
|
info!(
|
||||||
|
close_conn_sent = total,
|
||||||
|
"ME shutdown: RPC_CLOSE_CONN broadcast completed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!("ME shutdown: RPC_CLOSE_CONN broadcast timed out");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let shutdown_secs = shutdown_started_at.elapsed().as_secs();
|
||||||
|
info!(
|
||||||
|
"Shutdown completed successfully in {} {}.",
|
||||||
|
shutdown_secs,
|
||||||
|
unit_label(shutdown_secs, "second", "seconds")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dumps runtime statistics to the log.
|
||||||
|
fn dump_stats(stats: &Stats, process_started_at: Instant) {
|
||||||
|
let uptime_secs = process_started_at.elapsed().as_secs();
|
||||||
|
|
||||||
|
info!("=== Runtime Statistics Dump ===");
|
||||||
|
info!("Uptime: {}", format_uptime(uptime_secs));
|
||||||
|
|
||||||
|
// Connection stats
|
||||||
|
info!(
|
||||||
|
"Connections: total={}, current={} (direct={}, me={}), bad={}",
|
||||||
|
stats.get_connects_all(),
|
||||||
|
stats.get_current_connections_total(),
|
||||||
|
stats.get_current_connections_direct(),
|
||||||
|
stats.get_current_connections_me(),
|
||||||
|
stats.get_connects_bad(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ME pool stats
|
||||||
|
info!(
|
||||||
|
"ME keepalive: sent={}, pong={}, failed={}, timeout={}",
|
||||||
|
stats.get_me_keepalive_sent(),
|
||||||
|
stats.get_me_keepalive_pong(),
|
||||||
|
stats.get_me_keepalive_failed(),
|
||||||
|
stats.get_me_keepalive_timeout(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Relay stats
|
||||||
|
info!(
|
||||||
|
"Relay idle: soft_mark={}, hard_close={}, pressure_evict={}",
|
||||||
|
stats.get_relay_idle_soft_mark_total(),
|
||||||
|
stats.get_relay_idle_hard_close_total(),
|
||||||
|
stats.get_relay_pressure_evict_total(),
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("=== End Statistics Dump ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns a background task to handle operational signals (SIGUSR1, SIGUSR2).
|
||||||
|
///
|
||||||
|
/// These signals don't trigger shutdown but perform specific actions:
|
||||||
|
/// - SIGUSR1: Log rotation acknowledgment (for external log rotation tools)
|
||||||
|
/// - SIGUSR2: Dump runtime status to log
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(crate) fn spawn_signal_handlers(
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
process_started_at: Instant,
|
||||||
|
) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut sigusr1 = signal(SignalKind::user_defined1())
|
||||||
|
.expect("Failed to register SIGUSR1 handler");
|
||||||
|
let mut sigusr2 = signal(SignalKind::user_defined2())
|
||||||
|
.expect("Failed to register SIGUSR2 handler");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = sigusr1.recv() => {
|
||||||
|
handle_sigusr1();
|
||||||
|
}
|
||||||
|
_ = sigusr2.recv() => {
|
||||||
|
handle_sigusr2(&stats, process_started_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op on non-Unix platforms.
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub(crate) fn spawn_signal_handlers(
|
||||||
|
_stats: Arc<Stats>,
|
||||||
|
_process_started_at: Instant,
|
||||||
|
) {
|
||||||
|
// No SIGUSR1/SIGUSR2 on non-Unix
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles SIGUSR1 - log rotation signal.
|
||||||
|
///
|
||||||
|
/// This signal is typically sent by logrotate or similar tools after
|
||||||
|
/// rotating log files. Since tracing-subscriber doesn't natively support
|
||||||
|
/// reopening files, we just acknowledge the signal. If file logging is
|
||||||
|
/// added in the future, this would reopen log file handles.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn handle_sigusr1() {
|
||||||
|
info!("SIGUSR1 received - log rotation acknowledged");
|
||||||
|
// Future: If using file-based logging, reopen file handles here
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles SIGUSR2 - dump runtime status.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn handle_sigusr2(stats: &Stats, process_started_at: Instant) {
|
||||||
|
info!("SIGUSR2 received - dumping runtime status");
|
||||||
|
dump_stats(stats, process_started_at);
|
||||||
|
}
|
||||||
|
|||||||
+49
-3
@@ -4,8 +4,12 @@ mod api;
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod daemon;
|
||||||
mod error;
|
mod error;
|
||||||
mod ip_tracker;
|
mod ip_tracker;
|
||||||
|
mod logging;
|
||||||
|
mod service;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/ip_tracker_hotpath_adversarial_tests.rs"]
|
#[path = "tests/ip_tracker_hotpath_adversarial_tests.rs"]
|
||||||
mod ip_tracker_hotpath_adversarial_tests;
|
mod ip_tracker_hotpath_adversarial_tests;
|
||||||
@@ -27,7 +31,49 @@ mod tls_front;
|
|||||||
mod transport;
|
mod transport;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
#[tokio::main]
|
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
maestro::run().await
|
let cmd = cli::parse_command(&args);
|
||||||
|
|
||||||
|
// Handle subcommands that don't need the server (stop, reload, status, init)
|
||||||
|
if let Some(exit_code) = cli::execute_subcommand(&cmd) {
|
||||||
|
std::process::exit(exit_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Unix, handle daemonization before starting tokio runtime
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let daemon_opts = cmd.daemon_opts;
|
||||||
|
|
||||||
|
// Daemonize if requested (must happen before tokio runtime starts)
|
||||||
|
if daemon_opts.should_daemonize() {
|
||||||
|
match daemon::daemonize(daemon_opts.working_dir.as_deref()) {
|
||||||
|
Ok(daemon::DaemonizeResult::Parent) => {
|
||||||
|
// Parent process exits successfully
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Ok(daemon::DaemonizeResult::Child) => {
|
||||||
|
// Continue as daemon child
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[telemt] Daemonization failed: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now start tokio runtime and run the server
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?
|
||||||
|
.block_on(maestro::run_with_daemon(daemon_opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?
|
||||||
|
.block_on(maestro::run())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+482
@@ -935,6 +935,462 @@ async fn render_metrics(stats: &Stats, config: &ProxyConfig, ip_tracker: &UserIp
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_batches_total Total DC->Client flush batches"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_batches_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batches_total {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_batches_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_batch_frames_total Total DC->Client frames flushed in batches"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_batch_frames_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_frames_total {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_batch_frames_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_batch_bytes_total Total DC->Client bytes flushed in batches"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_batch_bytes_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_bytes_total {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_batch_bytes_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_flush_reason_total DC->Client flush reasons"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_flush_reason_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_reason_total{{reason=\"queue_drain\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_flush_reason_queue_drain_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_reason_total{{reason=\"batch_frames\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_flush_reason_batch_frames_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_reason_total{{reason=\"batch_bytes\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_flush_reason_batch_bytes_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_reason_total{{reason=\"max_delay\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_flush_reason_max_delay_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_reason_total{{reason=\"ack_immediate\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_flush_reason_ack_immediate_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_reason_total{{reason=\"close\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_flush_reason_close_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_data_frames_total DC->Client data frames"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_data_frames_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_data_frames_total {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_data_frames_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_ack_frames_total DC->Client quick-ack frames"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_ack_frames_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_ack_frames_total {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_ack_frames_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_payload_bytes_total DC->Client payload bytes before transport framing"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_payload_bytes_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_payload_bytes_total {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_payload_bytes_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_write_mode_total DC->Client writer mode selection"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_write_mode_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_write_mode_total{{mode=\"coalesced\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_write_mode_coalesced_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_write_mode_total{{mode=\"split\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_write_mode_split_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_quota_reject_total DC->Client quota rejects"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_quota_reject_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_quota_reject_total{{stage=\"pre_write\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_quota_reject_pre_write_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_quota_reject_total{{stage=\"post_write\"}} {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_quota_reject_post_write_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_frame_buf_shrink_total DC->Client reusable frame buffer shrink events"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_me_d2c_frame_buf_shrink_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_frame_buf_shrink_total {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_frame_buf_shrink_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_frame_buf_shrink_bytes_total DC->Client reusable frame buffer bytes released"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_me_d2c_frame_buf_shrink_bytes_total counter"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_frame_buf_shrink_bytes_total {}",
|
||||||
|
if me_allows_normal {
|
||||||
|
stats.get_me_d2c_frame_buf_shrink_bytes_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_batch_frames_bucket_total DC->Client batch frame count buckets"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_me_d2c_batch_frames_bucket_total counter"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_frames_bucket_total{{bucket=\"1\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_frames_bucket_1()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_frames_bucket_total{{bucket=\"2_4\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_frames_bucket_2_4()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_frames_bucket_total{{bucket=\"5_8\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_frames_bucket_5_8()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_frames_bucket_total{{bucket=\"9_16\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_frames_bucket_9_16()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_frames_bucket_total{{bucket=\"17_32\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_frames_bucket_17_32()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_frames_bucket_total{{bucket=\"gt_32\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_frames_bucket_gt_32()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_batch_bytes_bucket_total DC->Client batch byte size buckets"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_me_d2c_batch_bytes_bucket_total counter"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_bytes_bucket_total{{bucket=\"0_1k\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_bytes_bucket_0_1k()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_bytes_bucket_total{{bucket=\"1k_4k\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_bytes_bucket_1k_4k()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_bytes_bucket_total{{bucket=\"4k_16k\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_bytes_bucket_4k_16k()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_bytes_bucket_total{{bucket=\"16k_64k\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_bytes_bucket_16k_64k()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_bytes_bucket_total{{bucket=\"64k_128k\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_bytes_bucket_64k_128k()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_bytes_bucket_total{{bucket=\"gt_128k\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_bytes_bucket_gt_128k()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_flush_duration_us_bucket_total DC->Client flush duration buckets"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_me_d2c_flush_duration_us_bucket_total counter"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_duration_us_bucket_total{{bucket=\"0_50\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_flush_duration_us_bucket_0_50()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_duration_us_bucket_total{{bucket=\"51_200\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_flush_duration_us_bucket_51_200()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_duration_us_bucket_total{{bucket=\"201_1000\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_flush_duration_us_bucket_201_1000()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_duration_us_bucket_total{{bucket=\"1001_5000\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_flush_duration_us_bucket_1001_5000()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_duration_us_bucket_total{{bucket=\"5001_20000\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_flush_duration_us_bucket_5001_20000()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_flush_duration_us_bucket_total{{bucket=\"gt_20000\"}} {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_flush_duration_us_bucket_gt_20000()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_batch_timeout_armed_total DC->Client max-delay timer armed events"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_me_d2c_batch_timeout_armed_total counter"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_timeout_armed_total {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_timeout_armed_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_me_d2c_batch_timeout_fired_total DC->Client max-delay timer fired events"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_me_d2c_batch_timeout_fired_total counter"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_me_d2c_batch_timeout_fired_total {}",
|
||||||
|
if me_allows_debug {
|
||||||
|
stats.get_me_d2c_batch_timeout_fired_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"# HELP telemt_me_writer_pick_total ME writer-pick outcomes by mode and result"
|
"# HELP telemt_me_writer_pick_total ME writer-pick outcomes by mode and result"
|
||||||
@@ -2145,6 +2601,16 @@ mod tests {
|
|||||||
stats.increment_relay_idle_hard_close_total();
|
stats.increment_relay_idle_hard_close_total();
|
||||||
stats.increment_relay_pressure_evict_total();
|
stats.increment_relay_pressure_evict_total();
|
||||||
stats.increment_relay_protocol_desync_close_total();
|
stats.increment_relay_protocol_desync_close_total();
|
||||||
|
stats.increment_me_d2c_batches_total();
|
||||||
|
stats.add_me_d2c_batch_frames_total(3);
|
||||||
|
stats.add_me_d2c_batch_bytes_total(2048);
|
||||||
|
stats.increment_me_d2c_flush_reason(crate::stats::MeD2cFlushReason::AckImmediate);
|
||||||
|
stats.increment_me_d2c_data_frames_total();
|
||||||
|
stats.increment_me_d2c_ack_frames_total();
|
||||||
|
stats.add_me_d2c_payload_bytes_total(1800);
|
||||||
|
stats.increment_me_d2c_write_mode(crate::stats::MeD2cWriteMode::Coalesced);
|
||||||
|
stats.increment_me_d2c_quota_reject_total(crate::stats::MeD2cQuotaRejectStage::PostWrite);
|
||||||
|
stats.observe_me_d2c_frame_buf_shrink(4096);
|
||||||
stats.increment_user_connects("alice");
|
stats.increment_user_connects("alice");
|
||||||
stats.increment_user_curr_connects("alice");
|
stats.increment_user_curr_connects("alice");
|
||||||
stats.add_user_octets_from("alice", 1024);
|
stats.add_user_octets_from("alice", 1024);
|
||||||
@@ -2184,6 +2650,17 @@ mod tests {
|
|||||||
assert!(output.contains("telemt_relay_idle_hard_close_total 1"));
|
assert!(output.contains("telemt_relay_idle_hard_close_total 1"));
|
||||||
assert!(output.contains("telemt_relay_pressure_evict_total 1"));
|
assert!(output.contains("telemt_relay_pressure_evict_total 1"));
|
||||||
assert!(output.contains("telemt_relay_protocol_desync_close_total 1"));
|
assert!(output.contains("telemt_relay_protocol_desync_close_total 1"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_batches_total 1"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_batch_frames_total 3"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_batch_bytes_total 2048"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_flush_reason_total{reason=\"ack_immediate\"} 1"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_data_frames_total 1"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_ack_frames_total 1"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_payload_bytes_total 1800"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_write_mode_total{mode=\"coalesced\"} 1"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_quota_reject_total{stage=\"post_write\"} 1"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_frame_buf_shrink_total 1"));
|
||||||
|
assert!(output.contains("telemt_me_d2c_frame_buf_shrink_bytes_total 4096"));
|
||||||
assert!(output.contains("telemt_user_connections_total{user=\"alice\"} 1"));
|
assert!(output.contains("telemt_user_connections_total{user=\"alice\"} 1"));
|
||||||
assert!(output.contains("telemt_user_connections_current{user=\"alice\"} 1"));
|
assert!(output.contains("telemt_user_connections_current{user=\"alice\"} 1"));
|
||||||
assert!(output.contains("telemt_user_octets_from_client{user=\"alice\"} 1024"));
|
assert!(output.contains("telemt_user_octets_from_client{user=\"alice\"} 1024"));
|
||||||
@@ -2245,6 +2722,11 @@ mod tests {
|
|||||||
assert!(output.contains("# TYPE telemt_relay_idle_hard_close_total counter"));
|
assert!(output.contains("# TYPE telemt_relay_idle_hard_close_total counter"));
|
||||||
assert!(output.contains("# TYPE telemt_relay_pressure_evict_total counter"));
|
assert!(output.contains("# TYPE telemt_relay_pressure_evict_total counter"));
|
||||||
assert!(output.contains("# TYPE telemt_relay_protocol_desync_close_total counter"));
|
assert!(output.contains("# TYPE telemt_relay_protocol_desync_close_total counter"));
|
||||||
|
assert!(output.contains("# TYPE telemt_me_d2c_batches_total counter"));
|
||||||
|
assert!(output.contains("# TYPE telemt_me_d2c_flush_reason_total counter"));
|
||||||
|
assert!(output.contains("# TYPE telemt_me_d2c_write_mode_total counter"));
|
||||||
|
assert!(output.contains("# TYPE telemt_me_d2c_batch_frames_bucket_total counter"));
|
||||||
|
assert!(output.contains("# TYPE telemt_me_d2c_flush_duration_us_bucket_total counter"));
|
||||||
assert!(output.contains("# TYPE telemt_me_writer_removed_total counter"));
|
assert!(output.contains("# TYPE telemt_me_writer_removed_total counter"));
|
||||||
assert!(
|
assert!(
|
||||||
output
|
output
|
||||||
|
|||||||
+313
-55
@@ -21,7 +21,7 @@ use crate::proxy::route_mode::{
|
|||||||
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
||||||
cutover_stagger_delay,
|
cutover_stagger_delay,
|
||||||
};
|
};
|
||||||
use crate::stats::Stats;
|
use crate::stats::{MeD2cFlushReason, MeD2cQuotaRejectStage, MeD2cWriteMode, Stats};
|
||||||
use crate::stream::{BufferPool, CryptoReader, CryptoWriter, PooledBuffer};
|
use crate::stream::{BufferPool, CryptoReader, CryptoWriter, PooledBuffer};
|
||||||
use crate::transport::middle_proxy::{MePool, MeResponse, proto_flags_for_tag};
|
use crate::transport::middle_proxy::{MePool, MeResponse, proto_flags_for_tag};
|
||||||
|
|
||||||
@@ -45,6 +45,8 @@ const C2ME_SEND_TIMEOUT: Duration = Duration::from_millis(50);
|
|||||||
const C2ME_SEND_TIMEOUT: Duration = Duration::from_secs(5);
|
const C2ME_SEND_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
const ME_D2C_FLUSH_BATCH_MAX_FRAMES_MIN: usize = 1;
|
const ME_D2C_FLUSH_BATCH_MAX_FRAMES_MIN: usize = 1;
|
||||||
const ME_D2C_FLUSH_BATCH_MAX_BYTES_MIN: usize = 4096;
|
const ME_D2C_FLUSH_BATCH_MAX_BYTES_MIN: usize = 4096;
|
||||||
|
const ME_D2C_FRAME_BUF_SHRINK_HYSTERESIS_FACTOR: usize = 2;
|
||||||
|
const ME_D2C_SINGLE_WRITE_COALESCE_MAX_BYTES: usize = 128 * 1024;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const QUOTA_USER_LOCKS_MAX: usize = 64;
|
const QUOTA_USER_LOCKS_MAX: usize = 64;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@@ -214,6 +216,8 @@ struct MeD2cFlushPolicy {
|
|||||||
max_bytes: usize,
|
max_bytes: usize,
|
||||||
max_delay: Duration,
|
max_delay: Duration,
|
||||||
ack_flush_immediate: bool,
|
ack_flush_immediate: bool,
|
||||||
|
quota_soft_overshoot_bytes: u64,
|
||||||
|
frame_buf_shrink_threshold_bytes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -284,6 +288,11 @@ impl MeD2cFlushPolicy {
|
|||||||
.max(ME_D2C_FLUSH_BATCH_MAX_BYTES_MIN),
|
.max(ME_D2C_FLUSH_BATCH_MAX_BYTES_MIN),
|
||||||
max_delay: Duration::from_micros(config.general.me_d2c_flush_batch_max_delay_us),
|
max_delay: Duration::from_micros(config.general.me_d2c_flush_batch_max_delay_us),
|
||||||
ack_flush_immediate: config.general.me_d2c_ack_flush_immediate,
|
ack_flush_immediate: config.general.me_d2c_ack_flush_immediate,
|
||||||
|
quota_soft_overshoot_bytes: config.general.me_quota_soft_overshoot_bytes,
|
||||||
|
frame_buf_shrink_threshold_bytes: config
|
||||||
|
.general
|
||||||
|
.me_d2c_frame_buf_shrink_threshold_bytes
|
||||||
|
.max(4096),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,6 +535,7 @@ fn quota_exceeded_for_user(stats: &Stats, user: &str, quota_limit: Option<u64>)
|
|||||||
quota_limit.is_some_and(|quota| stats.get_user_total_octets(user) >= quota)
|
quota_limit.is_some_and(|quota| stats.get_user_total_octets(user) >= quota)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
fn quota_would_be_exceeded_for_user(
|
fn quota_would_be_exceeded_for_user(
|
||||||
stats: &Stats,
|
stats: &Stats,
|
||||||
user: &str,
|
user: &str,
|
||||||
@@ -538,6 +548,76 @@ fn quota_would_be_exceeded_for_user(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn quota_soft_cap(limit: u64, overshoot: u64) -> u64 {
|
||||||
|
limit.saturating_add(overshoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quota_exceeded_for_user_soft(
|
||||||
|
stats: &Stats,
|
||||||
|
user: &str,
|
||||||
|
quota_limit: Option<u64>,
|
||||||
|
overshoot: u64,
|
||||||
|
) -> bool {
|
||||||
|
quota_limit.is_some_and(|quota| stats.get_user_total_octets(user) >= quota_soft_cap(quota, overshoot))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quota_would_be_exceeded_for_user_soft(
|
||||||
|
stats: &Stats,
|
||||||
|
user: &str,
|
||||||
|
quota_limit: Option<u64>,
|
||||||
|
bytes: u64,
|
||||||
|
overshoot: u64,
|
||||||
|
) -> bool {
|
||||||
|
quota_limit.is_some_and(|quota| {
|
||||||
|
let cap = quota_soft_cap(quota, overshoot);
|
||||||
|
let used = stats.get_user_total_octets(user);
|
||||||
|
used >= cap || bytes > cap.saturating_sub(used)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_me_d2c_flush_reason(
|
||||||
|
flush_immediately: bool,
|
||||||
|
batch_frames: usize,
|
||||||
|
max_frames: usize,
|
||||||
|
batch_bytes: usize,
|
||||||
|
max_bytes: usize,
|
||||||
|
max_delay_fired: bool,
|
||||||
|
) -> MeD2cFlushReason {
|
||||||
|
if flush_immediately {
|
||||||
|
return MeD2cFlushReason::AckImmediate;
|
||||||
|
}
|
||||||
|
if batch_frames >= max_frames {
|
||||||
|
return MeD2cFlushReason::BatchFrames;
|
||||||
|
}
|
||||||
|
if batch_bytes >= max_bytes {
|
||||||
|
return MeD2cFlushReason::BatchBytes;
|
||||||
|
}
|
||||||
|
if max_delay_fired {
|
||||||
|
return MeD2cFlushReason::MaxDelay;
|
||||||
|
}
|
||||||
|
MeD2cFlushReason::QueueDrain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn observe_me_d2c_flush_event(
|
||||||
|
stats: &Stats,
|
||||||
|
reason: MeD2cFlushReason,
|
||||||
|
batch_frames: usize,
|
||||||
|
batch_bytes: usize,
|
||||||
|
flush_duration_us: Option<u64>,
|
||||||
|
) {
|
||||||
|
stats.increment_me_d2c_flush_reason(reason);
|
||||||
|
if batch_frames > 0 || batch_bytes > 0 {
|
||||||
|
stats.increment_me_d2c_batches_total();
|
||||||
|
stats.add_me_d2c_batch_frames_total(batch_frames as u64);
|
||||||
|
stats.add_me_d2c_batch_bytes_total(batch_bytes as u64);
|
||||||
|
stats.observe_me_d2c_batch_frames(batch_frames as u64);
|
||||||
|
stats.observe_me_d2c_batch_bytes(batch_bytes as u64);
|
||||||
|
}
|
||||||
|
if let Some(duration_us) = flush_duration_us {
|
||||||
|
stats.observe_me_d2c_flush_duration_us(duration_us);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn quota_user_lock_test_guard() -> &'static Mutex<()> {
|
fn quota_user_lock_test_guard() -> &'static Mutex<()> {
|
||||||
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||||
@@ -774,6 +854,7 @@ where
|
|||||||
let mut batch_frames = 0usize;
|
let mut batch_frames = 0usize;
|
||||||
let mut batch_bytes = 0usize;
|
let mut batch_bytes = 0usize;
|
||||||
let mut flush_immediately;
|
let mut flush_immediately;
|
||||||
|
let mut max_delay_fired = false;
|
||||||
|
|
||||||
let first_is_downstream_activity =
|
let first_is_downstream_activity =
|
||||||
matches!(&first, MeResponse::Data { .. } | MeResponse::Ack(_));
|
matches!(&first, MeResponse::Data { .. } | MeResponse::Ack(_));
|
||||||
@@ -786,6 +867,7 @@ where
|
|||||||
stats_clone.as_ref(),
|
stats_clone.as_ref(),
|
||||||
&user_clone,
|
&user_clone,
|
||||||
quota_limit,
|
quota_limit,
|
||||||
|
d2c_flush_policy.quota_soft_overshoot_bytes,
|
||||||
bytes_me2c_clone.as_ref(),
|
bytes_me2c_clone.as_ref(),
|
||||||
conn_id,
|
conn_id,
|
||||||
d2c_flush_policy.ack_flush_immediate,
|
d2c_flush_policy.ack_flush_immediate,
|
||||||
@@ -801,7 +883,25 @@ where
|
|||||||
flush_immediately = immediate;
|
flush_immediately = immediate;
|
||||||
}
|
}
|
||||||
MeWriterResponseOutcome::Close => {
|
MeWriterResponseOutcome::Close => {
|
||||||
|
let flush_started_at = if stats_clone.telemetry_policy().me_level.allows_debug() {
|
||||||
|
Some(Instant::now())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let _ = writer.flush().await;
|
let _ = writer.flush().await;
|
||||||
|
let flush_duration_us = flush_started_at.map(|started| {
|
||||||
|
started
|
||||||
|
.elapsed()
|
||||||
|
.as_micros()
|
||||||
|
.min(u128::from(u64::MAX)) as u64
|
||||||
|
});
|
||||||
|
observe_me_d2c_flush_event(
|
||||||
|
stats_clone.as_ref(),
|
||||||
|
MeD2cFlushReason::Close,
|
||||||
|
batch_frames,
|
||||||
|
batch_bytes,
|
||||||
|
flush_duration_us,
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -825,6 +925,7 @@ where
|
|||||||
stats_clone.as_ref(),
|
stats_clone.as_ref(),
|
||||||
&user_clone,
|
&user_clone,
|
||||||
quota_limit,
|
quota_limit,
|
||||||
|
d2c_flush_policy.quota_soft_overshoot_bytes,
|
||||||
bytes_me2c_clone.as_ref(),
|
bytes_me2c_clone.as_ref(),
|
||||||
conn_id,
|
conn_id,
|
||||||
d2c_flush_policy.ack_flush_immediate,
|
d2c_flush_policy.ack_flush_immediate,
|
||||||
@@ -840,7 +941,27 @@ where
|
|||||||
flush_immediately |= immediate;
|
flush_immediately |= immediate;
|
||||||
}
|
}
|
||||||
MeWriterResponseOutcome::Close => {
|
MeWriterResponseOutcome::Close => {
|
||||||
|
let flush_started_at =
|
||||||
|
if stats_clone.telemetry_policy().me_level.allows_debug() {
|
||||||
|
Some(Instant::now())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let _ = writer.flush().await;
|
let _ = writer.flush().await;
|
||||||
|
let flush_duration_us = flush_started_at.map(|started| {
|
||||||
|
started
|
||||||
|
.elapsed()
|
||||||
|
.as_micros()
|
||||||
|
.min(u128::from(u64::MAX))
|
||||||
|
as u64
|
||||||
|
});
|
||||||
|
observe_me_d2c_flush_event(
|
||||||
|
stats_clone.as_ref(),
|
||||||
|
MeD2cFlushReason::Close,
|
||||||
|
batch_frames,
|
||||||
|
batch_bytes,
|
||||||
|
flush_duration_us,
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -851,6 +972,7 @@ where
|
|||||||
&& batch_frames < d2c_flush_policy.max_frames
|
&& batch_frames < d2c_flush_policy.max_frames
|
||||||
&& batch_bytes < d2c_flush_policy.max_bytes
|
&& batch_bytes < d2c_flush_policy.max_bytes
|
||||||
{
|
{
|
||||||
|
stats_clone.increment_me_d2c_batch_timeout_armed_total();
|
||||||
match tokio::time::timeout(d2c_flush_policy.max_delay, me_rx_task.recv()).await {
|
match tokio::time::timeout(d2c_flush_policy.max_delay, me_rx_task.recv()).await {
|
||||||
Ok(Some(next)) => {
|
Ok(Some(next)) => {
|
||||||
let next_is_downstream_activity =
|
let next_is_downstream_activity =
|
||||||
@@ -864,6 +986,7 @@ where
|
|||||||
stats_clone.as_ref(),
|
stats_clone.as_ref(),
|
||||||
&user_clone,
|
&user_clone,
|
||||||
quota_limit,
|
quota_limit,
|
||||||
|
d2c_flush_policy.quota_soft_overshoot_bytes,
|
||||||
bytes_me2c_clone.as_ref(),
|
bytes_me2c_clone.as_ref(),
|
||||||
conn_id,
|
conn_id,
|
||||||
d2c_flush_policy.ack_flush_immediate,
|
d2c_flush_policy.ack_flush_immediate,
|
||||||
@@ -879,7 +1002,30 @@ where
|
|||||||
flush_immediately |= immediate;
|
flush_immediately |= immediate;
|
||||||
}
|
}
|
||||||
MeWriterResponseOutcome::Close => {
|
MeWriterResponseOutcome::Close => {
|
||||||
|
let flush_started_at = if stats_clone
|
||||||
|
.telemetry_policy()
|
||||||
|
.me_level
|
||||||
|
.allows_debug()
|
||||||
|
{
|
||||||
|
Some(Instant::now())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let _ = writer.flush().await;
|
let _ = writer.flush().await;
|
||||||
|
let flush_duration_us = flush_started_at.map(|started| {
|
||||||
|
started
|
||||||
|
.elapsed()
|
||||||
|
.as_micros()
|
||||||
|
.min(u128::from(u64::MAX))
|
||||||
|
as u64
|
||||||
|
});
|
||||||
|
observe_me_d2c_flush_event(
|
||||||
|
stats_clone.as_ref(),
|
||||||
|
MeD2cFlushReason::Close,
|
||||||
|
batch_frames,
|
||||||
|
batch_bytes,
|
||||||
|
flush_duration_us,
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -903,6 +1049,7 @@ where
|
|||||||
stats_clone.as_ref(),
|
stats_clone.as_ref(),
|
||||||
&user_clone,
|
&user_clone,
|
||||||
quota_limit,
|
quota_limit,
|
||||||
|
d2c_flush_policy.quota_soft_overshoot_bytes,
|
||||||
bytes_me2c_clone.as_ref(),
|
bytes_me2c_clone.as_ref(),
|
||||||
conn_id,
|
conn_id,
|
||||||
d2c_flush_policy.ack_flush_immediate,
|
d2c_flush_policy.ack_flush_immediate,
|
||||||
@@ -918,7 +1065,30 @@ where
|
|||||||
flush_immediately |= immediate;
|
flush_immediately |= immediate;
|
||||||
}
|
}
|
||||||
MeWriterResponseOutcome::Close => {
|
MeWriterResponseOutcome::Close => {
|
||||||
|
let flush_started_at = if stats_clone
|
||||||
|
.telemetry_policy()
|
||||||
|
.me_level
|
||||||
|
.allows_debug()
|
||||||
|
{
|
||||||
|
Some(Instant::now())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let _ = writer.flush().await;
|
let _ = writer.flush().await;
|
||||||
|
let flush_duration_us = flush_started_at.map(|started| {
|
||||||
|
started
|
||||||
|
.elapsed()
|
||||||
|
.as_micros()
|
||||||
|
.min(u128::from(u64::MAX))
|
||||||
|
as u64
|
||||||
|
});
|
||||||
|
observe_me_d2c_flush_event(
|
||||||
|
stats_clone.as_ref(),
|
||||||
|
MeD2cFlushReason::Close,
|
||||||
|
batch_frames,
|
||||||
|
batch_bytes,
|
||||||
|
flush_duration_us,
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -928,11 +1098,50 @@ where
|
|||||||
debug!(conn_id, "ME channel closed");
|
debug!(conn_id, "ME channel closed");
|
||||||
return Err(ProxyError::Proxy("ME connection lost".into()));
|
return Err(ProxyError::Proxy("ME connection lost".into()));
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
Err(_) => {
|
||||||
|
max_delay_fired = true;
|
||||||
|
stats_clone.increment_me_d2c_batch_timeout_fired_total();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let flush_reason = classify_me_d2c_flush_reason(
|
||||||
|
flush_immediately,
|
||||||
|
batch_frames,
|
||||||
|
d2c_flush_policy.max_frames,
|
||||||
|
batch_bytes,
|
||||||
|
d2c_flush_policy.max_bytes,
|
||||||
|
max_delay_fired,
|
||||||
|
);
|
||||||
|
let flush_started_at = if stats_clone.telemetry_policy().me_level.allows_debug() {
|
||||||
|
Some(Instant::now())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
writer.flush().await.map_err(ProxyError::Io)?;
|
writer.flush().await.map_err(ProxyError::Io)?;
|
||||||
|
let flush_duration_us = flush_started_at.map(|started| {
|
||||||
|
started
|
||||||
|
.elapsed()
|
||||||
|
.as_micros()
|
||||||
|
.min(u128::from(u64::MAX)) as u64
|
||||||
|
});
|
||||||
|
observe_me_d2c_flush_event(
|
||||||
|
stats_clone.as_ref(),
|
||||||
|
flush_reason,
|
||||||
|
batch_frames,
|
||||||
|
batch_bytes,
|
||||||
|
flush_duration_us,
|
||||||
|
);
|
||||||
|
let shrink_threshold = d2c_flush_policy.frame_buf_shrink_threshold_bytes;
|
||||||
|
let shrink_trigger = shrink_threshold
|
||||||
|
.saturating_mul(ME_D2C_FRAME_BUF_SHRINK_HYSTERESIS_FACTOR);
|
||||||
|
if frame_buf.capacity() > shrink_trigger {
|
||||||
|
let cap_before = frame_buf.capacity();
|
||||||
|
frame_buf.shrink_to(shrink_threshold);
|
||||||
|
let cap_after = frame_buf.capacity();
|
||||||
|
let bytes_freed = cap_before.saturating_sub(cap_after) as u64;
|
||||||
|
stats_clone.observe_me_d2c_frame_buf_shrink(bytes_freed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = &mut stop_rx => {
|
_ = &mut stop_rx => {
|
||||||
debug!(conn_id, "ME writer stop signal");
|
debug!(conn_id, "ME writer stop signal");
|
||||||
@@ -1482,6 +1691,7 @@ async fn process_me_writer_response<W>(
|
|||||||
stats: &Stats,
|
stats: &Stats,
|
||||||
user: &str,
|
user: &str,
|
||||||
quota_limit: Option<u64>,
|
quota_limit: Option<u64>,
|
||||||
|
quota_soft_overshoot_bytes: u64,
|
||||||
bytes_me2c: &AtomicU64,
|
bytes_me2c: &AtomicU64,
|
||||||
conn_id: u64,
|
conn_id: u64,
|
||||||
ack_flush_immediate: bool,
|
ack_flush_immediate: bool,
|
||||||
@@ -1498,31 +1708,39 @@ where
|
|||||||
trace!(conn_id, bytes = data.len(), flags, "ME->C data");
|
trace!(conn_id, bytes = data.len(), flags, "ME->C data");
|
||||||
}
|
}
|
||||||
let data_len = data.len() as u64;
|
let data_len = data.len() as u64;
|
||||||
if let Some(limit) = quota_limit {
|
if quota_would_be_exceeded_for_user_soft(
|
||||||
let quota_lock = quota_user_lock(user);
|
stats,
|
||||||
let _quota_guard = quota_lock.lock().await;
|
user,
|
||||||
if quota_would_be_exceeded_for_user(stats, user, Some(limit), data_len) {
|
quota_limit,
|
||||||
return Err(ProxyError::DataQuotaExceeded {
|
data_len,
|
||||||
user: user.to_string(),
|
quota_soft_overshoot_bytes,
|
||||||
});
|
) {
|
||||||
}
|
stats.increment_me_d2c_quota_reject_total(MeD2cQuotaRejectStage::PreWrite);
|
||||||
|
return Err(ProxyError::DataQuotaExceeded {
|
||||||
|
user: user.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let write_mode =
|
||||||
write_client_payload(client_writer, proto_tag, flags, &data, rng, frame_buf)
|
write_client_payload(client_writer, proto_tag, flags, &data, rng, frame_buf)
|
||||||
.await?;
|
.await?;
|
||||||
|
stats.increment_me_d2c_write_mode(write_mode);
|
||||||
|
|
||||||
bytes_me2c.fetch_add(data.len() as u64, Ordering::Relaxed);
|
bytes_me2c.fetch_add(data.len() as u64, Ordering::Relaxed);
|
||||||
stats.add_user_octets_to(user, data.len() as u64);
|
stats.add_user_octets_to(user, data.len() as u64);
|
||||||
|
stats.increment_me_d2c_data_frames_total();
|
||||||
|
stats.add_me_d2c_payload_bytes_total(data.len() as u64);
|
||||||
|
|
||||||
if quota_exceeded_for_user(stats, user, Some(limit)) {
|
if quota_exceeded_for_user_soft(
|
||||||
return Err(ProxyError::DataQuotaExceeded {
|
stats,
|
||||||
user: user.to_string(),
|
user,
|
||||||
});
|
quota_limit,
|
||||||
}
|
quota_soft_overshoot_bytes,
|
||||||
} else {
|
) {
|
||||||
write_client_payload(client_writer, proto_tag, flags, &data, rng, frame_buf)
|
stats.increment_me_d2c_quota_reject_total(MeD2cQuotaRejectStage::PostWrite);
|
||||||
.await?;
|
return Err(ProxyError::DataQuotaExceeded {
|
||||||
|
user: user.to_string(),
|
||||||
bytes_me2c.fetch_add(data.len() as u64, Ordering::Relaxed);
|
});
|
||||||
stats.add_user_octets_to(user, data.len() as u64);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(MeWriterResponseOutcome::Continue {
|
Ok(MeWriterResponseOutcome::Continue {
|
||||||
@@ -1538,6 +1756,7 @@ where
|
|||||||
trace!(conn_id, confirm, "ME->C quickack");
|
trace!(conn_id, confirm, "ME->C quickack");
|
||||||
}
|
}
|
||||||
write_client_ack(client_writer, proto_tag, confirm).await?;
|
write_client_ack(client_writer, proto_tag, confirm).await?;
|
||||||
|
stats.increment_me_d2c_ack_frames_total();
|
||||||
|
|
||||||
Ok(MeWriterResponseOutcome::Continue {
|
Ok(MeWriterResponseOutcome::Continue {
|
||||||
frames: 1,
|
frames: 1,
|
||||||
@@ -1588,13 +1807,13 @@ async fn write_client_payload<W>(
|
|||||||
data: &[u8],
|
data: &[u8],
|
||||||
rng: &SecureRandom,
|
rng: &SecureRandom,
|
||||||
frame_buf: &mut Vec<u8>,
|
frame_buf: &mut Vec<u8>,
|
||||||
) -> Result<()>
|
) -> Result<MeD2cWriteMode>
|
||||||
where
|
where
|
||||||
W: AsyncWrite + Unpin + Send + 'static,
|
W: AsyncWrite + Unpin + Send + 'static,
|
||||||
{
|
{
|
||||||
let quickack = (flags & RPC_FLAG_QUICKACK) != 0;
|
let quickack = (flags & RPC_FLAG_QUICKACK) != 0;
|
||||||
|
|
||||||
match proto_tag {
|
let write_mode = match proto_tag {
|
||||||
ProtoTag::Abridged => {
|
ProtoTag::Abridged => {
|
||||||
if !data.len().is_multiple_of(4) {
|
if !data.len().is_multiple_of(4) {
|
||||||
return Err(ProxyError::Proxy(format!(
|
return Err(ProxyError::Proxy(format!(
|
||||||
@@ -1609,28 +1828,46 @@ where
|
|||||||
if quickack {
|
if quickack {
|
||||||
first |= 0x80;
|
first |= 0x80;
|
||||||
}
|
}
|
||||||
frame_buf.clear();
|
let wire_len = 1usize.saturating_add(data.len());
|
||||||
frame_buf.reserve(1 + data.len());
|
if wire_len <= ME_D2C_SINGLE_WRITE_COALESCE_MAX_BYTES {
|
||||||
frame_buf.push(first);
|
frame_buf.clear();
|
||||||
frame_buf.extend_from_slice(data);
|
frame_buf.reserve(wire_len);
|
||||||
client_writer
|
frame_buf.push(first);
|
||||||
.write_all(frame_buf)
|
frame_buf.extend_from_slice(data);
|
||||||
.await
|
client_writer
|
||||||
.map_err(ProxyError::Io)?;
|
.write_all(frame_buf.as_slice())
|
||||||
|
.await
|
||||||
|
.map_err(ProxyError::Io)?;
|
||||||
|
MeD2cWriteMode::Coalesced
|
||||||
|
} else {
|
||||||
|
let header = [first];
|
||||||
|
client_writer.write_all(&header).await.map_err(ProxyError::Io)?;
|
||||||
|
client_writer.write_all(data).await.map_err(ProxyError::Io)?;
|
||||||
|
MeD2cWriteMode::Split
|
||||||
|
}
|
||||||
} else if len_words < (1 << 24) {
|
} else if len_words < (1 << 24) {
|
||||||
let mut first = 0x7fu8;
|
let mut first = 0x7fu8;
|
||||||
if quickack {
|
if quickack {
|
||||||
first |= 0x80;
|
first |= 0x80;
|
||||||
}
|
}
|
||||||
let lw = (len_words as u32).to_le_bytes();
|
let lw = (len_words as u32).to_le_bytes();
|
||||||
frame_buf.clear();
|
let wire_len = 4usize.saturating_add(data.len());
|
||||||
frame_buf.reserve(4 + data.len());
|
if wire_len <= ME_D2C_SINGLE_WRITE_COALESCE_MAX_BYTES {
|
||||||
frame_buf.extend_from_slice(&[first, lw[0], lw[1], lw[2]]);
|
frame_buf.clear();
|
||||||
frame_buf.extend_from_slice(data);
|
frame_buf.reserve(wire_len);
|
||||||
client_writer
|
frame_buf.extend_from_slice(&[first, lw[0], lw[1], lw[2]]);
|
||||||
.write_all(frame_buf)
|
frame_buf.extend_from_slice(data);
|
||||||
.await
|
client_writer
|
||||||
.map_err(ProxyError::Io)?;
|
.write_all(frame_buf.as_slice())
|
||||||
|
.await
|
||||||
|
.map_err(ProxyError::Io)?;
|
||||||
|
MeD2cWriteMode::Coalesced
|
||||||
|
} else {
|
||||||
|
let header = [first, lw[0], lw[1], lw[2]];
|
||||||
|
client_writer.write_all(&header).await.map_err(ProxyError::Io)?;
|
||||||
|
client_writer.write_all(data).await.map_err(ProxyError::Io)?;
|
||||||
|
MeD2cWriteMode::Split
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(ProxyError::Proxy(format!(
|
return Err(ProxyError::Proxy(format!(
|
||||||
"Abridged frame too large: {}",
|
"Abridged frame too large: {}",
|
||||||
@@ -1650,25 +1887,46 @@ where
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
let (len_val, total) =
|
let (len_val, total) =
|
||||||
compute_intermediate_secure_wire_len(data.len(), padding_len, quickack)?;
|
compute_intermediate_secure_wire_len(data.len(), padding_len, quickack)?;
|
||||||
frame_buf.clear();
|
if total <= ME_D2C_SINGLE_WRITE_COALESCE_MAX_BYTES {
|
||||||
frame_buf.reserve(total);
|
frame_buf.clear();
|
||||||
frame_buf.extend_from_slice(&len_val.to_le_bytes());
|
frame_buf.reserve(total);
|
||||||
frame_buf.extend_from_slice(data);
|
frame_buf.extend_from_slice(&len_val.to_le_bytes());
|
||||||
if padding_len > 0 {
|
frame_buf.extend_from_slice(data);
|
||||||
let start = frame_buf.len();
|
if padding_len > 0 {
|
||||||
frame_buf.resize(start + padding_len, 0);
|
let start = frame_buf.len();
|
||||||
rng.fill(&mut frame_buf[start..]);
|
frame_buf.resize(start + padding_len, 0);
|
||||||
|
rng.fill(&mut frame_buf[start..]);
|
||||||
|
}
|
||||||
|
client_writer
|
||||||
|
.write_all(frame_buf.as_slice())
|
||||||
|
.await
|
||||||
|
.map_err(ProxyError::Io)?;
|
||||||
|
MeD2cWriteMode::Coalesced
|
||||||
|
} else {
|
||||||
|
let header = len_val.to_le_bytes();
|
||||||
|
client_writer.write_all(&header).await.map_err(ProxyError::Io)?;
|
||||||
|
client_writer.write_all(data).await.map_err(ProxyError::Io)?;
|
||||||
|
if padding_len > 0 {
|
||||||
|
frame_buf.clear();
|
||||||
|
if frame_buf.capacity() < padding_len {
|
||||||
|
frame_buf.reserve(padding_len);
|
||||||
|
}
|
||||||
|
frame_buf.resize(padding_len, 0);
|
||||||
|
rng.fill(frame_buf.as_mut_slice());
|
||||||
|
client_writer
|
||||||
|
.write_all(frame_buf.as_slice())
|
||||||
|
.await
|
||||||
|
.map_err(ProxyError::Io)?;
|
||||||
|
}
|
||||||
|
MeD2cWriteMode::Split
|
||||||
}
|
}
|
||||||
client_writer
|
|
||||||
.write_all(frame_buf)
|
|
||||||
.await
|
|
||||||
.map_err(ProxyError::Io)?;
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(write_mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_client_ack<W>(
|
async fn write_client_ack<W>(
|
||||||
|
|||||||
@@ -1540,6 +1540,7 @@ async fn process_me_writer_response_ack_obeys_flush_policy() {
|
|||||||
&stats,
|
&stats,
|
||||||
"user",
|
"user",
|
||||||
None,
|
None,
|
||||||
|
0,
|
||||||
&bytes_me2c,
|
&bytes_me2c,
|
||||||
77,
|
77,
|
||||||
true,
|
true,
|
||||||
@@ -1566,6 +1567,7 @@ async fn process_me_writer_response_ack_obeys_flush_policy() {
|
|||||||
&stats,
|
&stats,
|
||||||
"user",
|
"user",
|
||||||
None,
|
None,
|
||||||
|
0,
|
||||||
&bytes_me2c,
|
&bytes_me2c,
|
||||||
77,
|
77,
|
||||||
false,
|
false,
|
||||||
@@ -1606,6 +1608,7 @@ async fn process_me_writer_response_data_updates_byte_accounting() {
|
|||||||
&stats,
|
&stats,
|
||||||
"user",
|
"user",
|
||||||
None,
|
None,
|
||||||
|
0,
|
||||||
&bytes_me2c,
|
&bytes_me2c,
|
||||||
88,
|
88,
|
||||||
false,
|
false,
|
||||||
@@ -1652,6 +1655,7 @@ async fn process_me_writer_response_data_enforces_live_user_quota() {
|
|||||||
&stats,
|
&stats,
|
||||||
"quota-user",
|
"quota-user",
|
||||||
Some(12),
|
Some(12),
|
||||||
|
0,
|
||||||
&bytes_me2c,
|
&bytes_me2c,
|
||||||
89,
|
89,
|
||||||
false,
|
false,
|
||||||
@@ -1700,6 +1704,7 @@ async fn process_me_writer_response_concurrent_same_user_quota_does_not_overshoo
|
|||||||
&stats,
|
&stats,
|
||||||
user,
|
user,
|
||||||
Some(1),
|
Some(1),
|
||||||
|
0,
|
||||||
&bytes_me2c,
|
&bytes_me2c,
|
||||||
91,
|
91,
|
||||||
false,
|
false,
|
||||||
@@ -1717,6 +1722,7 @@ async fn process_me_writer_response_concurrent_same_user_quota_does_not_overshoo
|
|||||||
&stats,
|
&stats,
|
||||||
user,
|
user,
|
||||||
Some(1),
|
Some(1),
|
||||||
|
0,
|
||||||
&bytes_me2c,
|
&bytes_me2c,
|
||||||
92,
|
92,
|
||||||
false,
|
false,
|
||||||
@@ -1765,6 +1771,7 @@ async fn process_me_writer_response_data_does_not_forward_partial_payload_when_r
|
|||||||
&stats,
|
&stats,
|
||||||
"partial-quota-user",
|
"partial-quota-user",
|
||||||
Some(4),
|
Some(4),
|
||||||
|
0,
|
||||||
&bytes_me2c,
|
&bytes_me2c,
|
||||||
90,
|
90,
|
||||||
false,
|
false,
|
||||||
@@ -1970,6 +1977,7 @@ async fn run_quota_race_attempt(
|
|||||||
stats,
|
stats,
|
||||||
user,
|
user,
|
||||||
Some(1),
|
Some(1),
|
||||||
|
0,
|
||||||
bytes_me2c,
|
bytes_me2c,
|
||||||
conn_id,
|
conn_id,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
//! Service manager integration for telemt.
|
||||||
|
//!
|
||||||
|
//! Supports generating service files for:
|
||||||
|
//! - systemd (Linux)
|
||||||
|
//! - OpenRC (Alpine, Gentoo)
|
||||||
|
//! - rc.d (FreeBSD)
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Detected init/service system.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum InitSystem {
|
||||||
|
/// systemd (most modern Linux distributions)
|
||||||
|
Systemd,
|
||||||
|
/// OpenRC (Alpine, Gentoo, some BSDs)
|
||||||
|
OpenRC,
|
||||||
|
/// FreeBSD rc.d
|
||||||
|
FreeBSDRc,
|
||||||
|
/// No known init system detected
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for InitSystem {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
InitSystem::Systemd => write!(f, "systemd"),
|
||||||
|
InitSystem::OpenRC => write!(f, "OpenRC"),
|
||||||
|
InitSystem::FreeBSDRc => write!(f, "FreeBSD rc.d"),
|
||||||
|
InitSystem::Unknown => write!(f, "unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detects the init system in use on the current host.
|
||||||
|
pub fn detect_init_system() -> InitSystem {
|
||||||
|
// Check for systemd first (most common on Linux)
|
||||||
|
if Path::new("/run/systemd/system").exists() {
|
||||||
|
return InitSystem::Systemd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for OpenRC
|
||||||
|
if Path::new("/sbin/openrc-run").exists() || Path::new("/sbin/openrc").exists() {
|
||||||
|
return InitSystem::OpenRC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for FreeBSD rc.d
|
||||||
|
if Path::new("/etc/rc.subr").exists() && Path::new("/etc/rc.d").exists() {
|
||||||
|
return InitSystem::FreeBSDRc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check if systemctl exists even without /run/systemd
|
||||||
|
if Path::new("/usr/bin/systemctl").exists() || Path::new("/bin/systemctl").exists() {
|
||||||
|
return InitSystem::Systemd;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitSystem::Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default service file path for the given init system.
|
||||||
|
pub fn service_file_path(init_system: InitSystem) -> &'static str {
|
||||||
|
match init_system {
|
||||||
|
InitSystem::Systemd => "/etc/systemd/system/telemt.service",
|
||||||
|
InitSystem::OpenRC => "/etc/init.d/telemt",
|
||||||
|
InitSystem::FreeBSDRc => "/usr/local/etc/rc.d/telemt",
|
||||||
|
InitSystem::Unknown => "/etc/init.d/telemt",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for generating service files.
|
||||||
|
pub struct ServiceOptions<'a> {
|
||||||
|
/// Path to the telemt executable
|
||||||
|
pub exe_path: &'a Path,
|
||||||
|
/// Path to the configuration file
|
||||||
|
pub config_path: &'a Path,
|
||||||
|
/// User to run as (optional)
|
||||||
|
pub user: Option<&'a str>,
|
||||||
|
/// Group to run as (optional)
|
||||||
|
pub group: Option<&'a str>,
|
||||||
|
/// PID file path
|
||||||
|
pub pid_file: &'a str,
|
||||||
|
/// Working directory
|
||||||
|
pub working_dir: Option<&'a str>,
|
||||||
|
/// Description
|
||||||
|
pub description: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for ServiceOptions<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
exe_path: Path::new("/usr/local/bin/telemt"),
|
||||||
|
config_path: Path::new("/etc/telemt/config.toml"),
|
||||||
|
user: Some("telemt"),
|
||||||
|
group: Some("telemt"),
|
||||||
|
pid_file: "/var/run/telemt.pid",
|
||||||
|
working_dir: Some("/var/lib/telemt"),
|
||||||
|
description: "Telemt MTProxy - Telegram MTProto Proxy",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a service file for the given init system.
|
||||||
|
pub fn generate_service_file(init_system: InitSystem, opts: &ServiceOptions) -> String {
|
||||||
|
match init_system {
|
||||||
|
InitSystem::Systemd => generate_systemd_unit(opts),
|
||||||
|
InitSystem::OpenRC => generate_openrc_script(opts),
|
||||||
|
InitSystem::FreeBSDRc => generate_freebsd_rc_script(opts),
|
||||||
|
InitSystem::Unknown => generate_systemd_unit(opts), // Default to systemd format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates an enhanced systemd unit file.
|
||||||
|
fn generate_systemd_unit(opts: &ServiceOptions) -> String {
|
||||||
|
let user_line = opts.user.map(|u| format!("User={}", u)).unwrap_or_default();
|
||||||
|
let group_line = opts.group.map(|g| format!("Group={}", g)).unwrap_or_default();
|
||||||
|
let working_dir = opts.working_dir.map(|d| format!("WorkingDirectory={}", d)).unwrap_or_default();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"[Unit]
|
||||||
|
Description={description}
|
||||||
|
Documentation=https://github.com/telemt/telemt
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart={exe} --foreground --pid-file {pid_file} {config}
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
PIDFile={pid_file}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
{user}
|
||||||
|
{group}
|
||||||
|
{working_dir}
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
LimitNOFILE=65535
|
||||||
|
LimitNPROC=4096
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
LockPersonality=true
|
||||||
|
|
||||||
|
# Allow binding to privileged ports and writing to specific paths
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
ReadWritePaths=/etc/telemt /var/run /var/lib/telemt
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"#,
|
||||||
|
description = opts.description,
|
||||||
|
exe = opts.exe_path.display(),
|
||||||
|
config = opts.config_path.display(),
|
||||||
|
pid_file = opts.pid_file,
|
||||||
|
user = user_line,
|
||||||
|
group = group_line,
|
||||||
|
working_dir = working_dir,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates an OpenRC init script.
|
||||||
|
fn generate_openrc_script(opts: &ServiceOptions) -> String {
|
||||||
|
let user = opts.user.unwrap_or("root");
|
||||||
|
let group = opts.group.unwrap_or("root");
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"#!/sbin/openrc-run
|
||||||
|
# OpenRC init script for telemt
|
||||||
|
|
||||||
|
description="{description}"
|
||||||
|
command="{exe}"
|
||||||
|
command_args="--daemon --syslog --pid-file {pid_file} {config}"
|
||||||
|
command_user="{user}:{group}"
|
||||||
|
pidfile="{pid_file}"
|
||||||
|
|
||||||
|
depend() {{
|
||||||
|
need net
|
||||||
|
use logger
|
||||||
|
after firewall
|
||||||
|
}}
|
||||||
|
|
||||||
|
start_pre() {{
|
||||||
|
checkpath --directory --owner {user}:{group} --mode 0755 /var/run
|
||||||
|
checkpath --directory --owner {user}:{group} --mode 0755 /var/lib/telemt
|
||||||
|
checkpath --directory --owner {user}:{group} --mode 0755 /var/log/telemt
|
||||||
|
}}
|
||||||
|
|
||||||
|
reload() {{
|
||||||
|
ebegin "Reloading ${{RC_SVCNAME}}"
|
||||||
|
start-stop-daemon --signal HUP --pidfile "${{pidfile}}"
|
||||||
|
eend $?
|
||||||
|
}}
|
||||||
|
"#,
|
||||||
|
description = opts.description,
|
||||||
|
exe = opts.exe_path.display(),
|
||||||
|
config = opts.config_path.display(),
|
||||||
|
pid_file = opts.pid_file,
|
||||||
|
user = user,
|
||||||
|
group = group,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a FreeBSD rc.d script.
|
||||||
|
fn generate_freebsd_rc_script(opts: &ServiceOptions) -> String {
|
||||||
|
let user = opts.user.unwrap_or("root");
|
||||||
|
let group = opts.group.unwrap_or("wheel");
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"#!/bin/sh
|
||||||
|
#
|
||||||
|
# PROVIDE: telemt
|
||||||
|
# REQUIRE: LOGIN NETWORKING
|
||||||
|
# KEYWORD: shutdown
|
||||||
|
#
|
||||||
|
# Add the following lines to /etc/rc.conf to enable telemt:
|
||||||
|
#
|
||||||
|
# telemt_enable="YES"
|
||||||
|
# telemt_config="/etc/telemt/config.toml" # optional
|
||||||
|
# telemt_user="telemt" # optional
|
||||||
|
# telemt_group="telemt" # optional
|
||||||
|
#
|
||||||
|
|
||||||
|
. /etc/rc.subr
|
||||||
|
|
||||||
|
name="telemt"
|
||||||
|
rcvar="telemt_enable"
|
||||||
|
desc="{description}"
|
||||||
|
|
||||||
|
load_rc_config $name
|
||||||
|
|
||||||
|
: ${{telemt_enable:="NO"}}
|
||||||
|
: ${{telemt_config:="{config}"}}
|
||||||
|
: ${{telemt_user:="{user}"}}
|
||||||
|
: ${{telemt_group:="{group}"}}
|
||||||
|
: ${{telemt_pidfile:="{pid_file}"}}
|
||||||
|
|
||||||
|
pidfile="${{telemt_pidfile}}"
|
||||||
|
command="{exe}"
|
||||||
|
command_args="--daemon --syslog --pid-file ${{telemt_pidfile}} ${{telemt_config}}"
|
||||||
|
|
||||||
|
start_precmd="telemt_prestart"
|
||||||
|
reload_cmd="telemt_reload"
|
||||||
|
extra_commands="reload"
|
||||||
|
|
||||||
|
telemt_prestart() {{
|
||||||
|
install -d -o ${{telemt_user}} -g ${{telemt_group}} -m 755 /var/run
|
||||||
|
install -d -o ${{telemt_user}} -g ${{telemt_group}} -m 755 /var/lib/telemt
|
||||||
|
}}
|
||||||
|
|
||||||
|
telemt_reload() {{
|
||||||
|
if [ -f "${{pidfile}}" ]; then
|
||||||
|
echo "Reloading ${{name}} configuration."
|
||||||
|
kill -HUP $(cat ${{pidfile}})
|
||||||
|
else
|
||||||
|
echo "${{name}} is not running."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}}
|
||||||
|
|
||||||
|
run_rc_command "$1"
|
||||||
|
"#,
|
||||||
|
description = opts.description,
|
||||||
|
exe = opts.exe_path.display(),
|
||||||
|
config = opts.config_path.display(),
|
||||||
|
pid_file = opts.pid_file,
|
||||||
|
user = user,
|
||||||
|
group = group,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installation instructions for each init system.
|
||||||
|
pub fn installation_instructions(init_system: InitSystem) -> &'static str {
|
||||||
|
match init_system {
|
||||||
|
InitSystem::Systemd => {
|
||||||
|
r#"To install and enable the service:
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable telemt
|
||||||
|
sudo systemctl start telemt
|
||||||
|
|
||||||
|
To check status:
|
||||||
|
sudo systemctl status telemt
|
||||||
|
|
||||||
|
To view logs:
|
||||||
|
journalctl -u telemt -f
|
||||||
|
|
||||||
|
To reload configuration:
|
||||||
|
sudo systemctl reload telemt
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
InitSystem::OpenRC => {
|
||||||
|
r#"To install and enable the service:
|
||||||
|
sudo chmod +x /etc/init.d/telemt
|
||||||
|
sudo rc-update add telemt default
|
||||||
|
sudo rc-service telemt start
|
||||||
|
|
||||||
|
To check status:
|
||||||
|
sudo rc-service telemt status
|
||||||
|
|
||||||
|
To reload configuration:
|
||||||
|
sudo rc-service telemt reload
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
InitSystem::FreeBSDRc => {
|
||||||
|
r#"To install and enable the service:
|
||||||
|
sudo chmod +x /usr/local/etc/rc.d/telemt
|
||||||
|
sudo sysrc telemt_enable="YES"
|
||||||
|
sudo service telemt start
|
||||||
|
|
||||||
|
To check status:
|
||||||
|
sudo service telemt status
|
||||||
|
|
||||||
|
To reload configuration:
|
||||||
|
sudo service telemt reload
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
InitSystem::Unknown => {
|
||||||
|
r#"No supported init system detected.
|
||||||
|
You may need to create a service file manually or run telemt directly:
|
||||||
|
telemt start /etc/telemt/config.toml
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_systemd_unit_generation() {
|
||||||
|
let opts = ServiceOptions::default();
|
||||||
|
let unit = generate_systemd_unit(&opts);
|
||||||
|
assert!(unit.contains("[Unit]"));
|
||||||
|
assert!(unit.contains("[Service]"));
|
||||||
|
assert!(unit.contains("[Install]"));
|
||||||
|
assert!(unit.contains("ExecReload="));
|
||||||
|
assert!(unit.contains("PIDFile="));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_openrc_script_generation() {
|
||||||
|
let opts = ServiceOptions::default();
|
||||||
|
let script = generate_openrc_script(&opts);
|
||||||
|
assert!(script.contains("#!/sbin/openrc-run"));
|
||||||
|
assert!(script.contains("depend()"));
|
||||||
|
assert!(script.contains("reload()"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_freebsd_rc_script_generation() {
|
||||||
|
let opts = ServiceOptions::default();
|
||||||
|
let script = generate_freebsd_rc_script(&opts);
|
||||||
|
assert!(script.contains("#!/bin/sh"));
|
||||||
|
assert!(script.contains("PROVIDE: telemt"));
|
||||||
|
assert!(script.contains("run_rc_command"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_file_paths() {
|
||||||
|
assert_eq!(service_file_path(InitSystem::Systemd), "/etc/systemd/system/telemt.service");
|
||||||
|
assert_eq!(service_file_path(InitSystem::OpenRC), "/etc/init.d/telemt");
|
||||||
|
assert_eq!(service_file_path(InitSystem::FreeBSDRc), "/usr/local/etc/rc.d/telemt");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,28 @@ enum RouteConnectionGauge {
|
|||||||
Middle,
|
Middle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum MeD2cFlushReason {
|
||||||
|
QueueDrain,
|
||||||
|
BatchFrames,
|
||||||
|
BatchBytes,
|
||||||
|
MaxDelay,
|
||||||
|
AckImmediate,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum MeD2cWriteMode {
|
||||||
|
Coalesced,
|
||||||
|
Split,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum MeD2cQuotaRejectStage {
|
||||||
|
PreWrite,
|
||||||
|
PostWrite,
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use = "RouteConnectionLease must be kept alive to hold the connection gauge increment"]
|
#[must_use = "RouteConnectionLease must be kept alive to hold the connection gauge increment"]
|
||||||
pub struct RouteConnectionLease {
|
pub struct RouteConnectionLease {
|
||||||
stats: Arc<Stats>,
|
stats: Arc<Stats>,
|
||||||
@@ -140,6 +162,44 @@ pub struct Stats {
|
|||||||
me_route_drop_queue_full: AtomicU64,
|
me_route_drop_queue_full: AtomicU64,
|
||||||
me_route_drop_queue_full_base: AtomicU64,
|
me_route_drop_queue_full_base: AtomicU64,
|
||||||
me_route_drop_queue_full_high: AtomicU64,
|
me_route_drop_queue_full_high: AtomicU64,
|
||||||
|
me_d2c_batches_total: AtomicU64,
|
||||||
|
me_d2c_batch_frames_total: AtomicU64,
|
||||||
|
me_d2c_batch_bytes_total: AtomicU64,
|
||||||
|
me_d2c_flush_reason_queue_drain_total: AtomicU64,
|
||||||
|
me_d2c_flush_reason_batch_frames_total: AtomicU64,
|
||||||
|
me_d2c_flush_reason_batch_bytes_total: AtomicU64,
|
||||||
|
me_d2c_flush_reason_max_delay_total: AtomicU64,
|
||||||
|
me_d2c_flush_reason_ack_immediate_total: AtomicU64,
|
||||||
|
me_d2c_flush_reason_close_total: AtomicU64,
|
||||||
|
me_d2c_data_frames_total: AtomicU64,
|
||||||
|
me_d2c_ack_frames_total: AtomicU64,
|
||||||
|
me_d2c_payload_bytes_total: AtomicU64,
|
||||||
|
me_d2c_write_mode_coalesced_total: AtomicU64,
|
||||||
|
me_d2c_write_mode_split_total: AtomicU64,
|
||||||
|
me_d2c_quota_reject_pre_write_total: AtomicU64,
|
||||||
|
me_d2c_quota_reject_post_write_total: AtomicU64,
|
||||||
|
me_d2c_frame_buf_shrink_total: AtomicU64,
|
||||||
|
me_d2c_frame_buf_shrink_bytes_total: AtomicU64,
|
||||||
|
me_d2c_batch_frames_bucket_1: AtomicU64,
|
||||||
|
me_d2c_batch_frames_bucket_2_4: AtomicU64,
|
||||||
|
me_d2c_batch_frames_bucket_5_8: AtomicU64,
|
||||||
|
me_d2c_batch_frames_bucket_9_16: AtomicU64,
|
||||||
|
me_d2c_batch_frames_bucket_17_32: AtomicU64,
|
||||||
|
me_d2c_batch_frames_bucket_gt_32: AtomicU64,
|
||||||
|
me_d2c_batch_bytes_bucket_0_1k: AtomicU64,
|
||||||
|
me_d2c_batch_bytes_bucket_1k_4k: AtomicU64,
|
||||||
|
me_d2c_batch_bytes_bucket_4k_16k: AtomicU64,
|
||||||
|
me_d2c_batch_bytes_bucket_16k_64k: AtomicU64,
|
||||||
|
me_d2c_batch_bytes_bucket_64k_128k: AtomicU64,
|
||||||
|
me_d2c_batch_bytes_bucket_gt_128k: AtomicU64,
|
||||||
|
me_d2c_flush_duration_us_bucket_0_50: AtomicU64,
|
||||||
|
me_d2c_flush_duration_us_bucket_51_200: AtomicU64,
|
||||||
|
me_d2c_flush_duration_us_bucket_201_1000: AtomicU64,
|
||||||
|
me_d2c_flush_duration_us_bucket_1001_5000: AtomicU64,
|
||||||
|
me_d2c_flush_duration_us_bucket_5001_20000: AtomicU64,
|
||||||
|
me_d2c_flush_duration_us_bucket_gt_20000: AtomicU64,
|
||||||
|
me_d2c_batch_timeout_armed_total: AtomicU64,
|
||||||
|
me_d2c_batch_timeout_fired_total: AtomicU64,
|
||||||
me_writer_pick_sorted_rr_success_try_total: AtomicU64,
|
me_writer_pick_sorted_rr_success_try_total: AtomicU64,
|
||||||
me_writer_pick_sorted_rr_success_fallback_total: AtomicU64,
|
me_writer_pick_sorted_rr_success_fallback_total: AtomicU64,
|
||||||
me_writer_pick_sorted_rr_full_total: AtomicU64,
|
me_writer_pick_sorted_rr_full_total: AtomicU64,
|
||||||
@@ -594,6 +654,215 @@ impl Stats {
|
|||||||
.fetch_add(1, Ordering::Relaxed);
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn increment_me_d2c_batches_total(&self) {
|
||||||
|
if self.telemetry_me_allows_normal() {
|
||||||
|
self.me_d2c_batches_total.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn add_me_d2c_batch_frames_total(&self, frames: u64) {
|
||||||
|
if self.telemetry_me_allows_normal() {
|
||||||
|
self.me_d2c_batch_frames_total
|
||||||
|
.fetch_add(frames, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn add_me_d2c_batch_bytes_total(&self, bytes: u64) {
|
||||||
|
if self.telemetry_me_allows_normal() {
|
||||||
|
self.me_d2c_batch_bytes_total
|
||||||
|
.fetch_add(bytes, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn increment_me_d2c_flush_reason(&self, reason: MeD2cFlushReason) {
|
||||||
|
if !self.telemetry_me_allows_normal() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match reason {
|
||||||
|
MeD2cFlushReason::QueueDrain => {
|
||||||
|
self.me_d2c_flush_reason_queue_drain_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
MeD2cFlushReason::BatchFrames => {
|
||||||
|
self.me_d2c_flush_reason_batch_frames_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
MeD2cFlushReason::BatchBytes => {
|
||||||
|
self.me_d2c_flush_reason_batch_bytes_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
MeD2cFlushReason::MaxDelay => {
|
||||||
|
self.me_d2c_flush_reason_max_delay_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
MeD2cFlushReason::AckImmediate => {
|
||||||
|
self.me_d2c_flush_reason_ack_immediate_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
MeD2cFlushReason::Close => {
|
||||||
|
self.me_d2c_flush_reason_close_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn increment_me_d2c_data_frames_total(&self) {
|
||||||
|
if self.telemetry_me_allows_normal() {
|
||||||
|
self.me_d2c_data_frames_total.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn increment_me_d2c_ack_frames_total(&self) {
|
||||||
|
if self.telemetry_me_allows_normal() {
|
||||||
|
self.me_d2c_ack_frames_total.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn add_me_d2c_payload_bytes_total(&self, bytes: u64) {
|
||||||
|
if self.telemetry_me_allows_normal() {
|
||||||
|
self.me_d2c_payload_bytes_total
|
||||||
|
.fetch_add(bytes, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn increment_me_d2c_write_mode(&self, mode: MeD2cWriteMode) {
|
||||||
|
if !self.telemetry_me_allows_normal() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match mode {
|
||||||
|
MeD2cWriteMode::Coalesced => {
|
||||||
|
self.me_d2c_write_mode_coalesced_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
MeD2cWriteMode::Split => {
|
||||||
|
self.me_d2c_write_mode_split_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn increment_me_d2c_quota_reject_total(&self, stage: MeD2cQuotaRejectStage) {
|
||||||
|
if !self.telemetry_me_allows_normal() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match stage {
|
||||||
|
MeD2cQuotaRejectStage::PreWrite => {
|
||||||
|
self.me_d2c_quota_reject_pre_write_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
MeD2cQuotaRejectStage::PostWrite => {
|
||||||
|
self.me_d2c_quota_reject_post_write_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn observe_me_d2c_frame_buf_shrink(&self, bytes_freed: u64) {
|
||||||
|
if !self.telemetry_me_allows_normal() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.me_d2c_frame_buf_shrink_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.me_d2c_frame_buf_shrink_bytes_total
|
||||||
|
.fetch_add(bytes_freed, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
pub fn observe_me_d2c_batch_frames(&self, frames: u64) {
|
||||||
|
if !self.telemetry_me_allows_debug() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match frames {
|
||||||
|
0 => {}
|
||||||
|
1 => {
|
||||||
|
self.me_d2c_batch_frames_bucket_1
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
2..=4 => {
|
||||||
|
self.me_d2c_batch_frames_bucket_2_4
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
5..=8 => {
|
||||||
|
self.me_d2c_batch_frames_bucket_5_8
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
9..=16 => {
|
||||||
|
self.me_d2c_batch_frames_bucket_9_16
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
17..=32 => {
|
||||||
|
self.me_d2c_batch_frames_bucket_17_32
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.me_d2c_batch_frames_bucket_gt_32
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn observe_me_d2c_batch_bytes(&self, bytes: u64) {
|
||||||
|
if !self.telemetry_me_allows_debug() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match bytes {
|
||||||
|
0..=1024 => {
|
||||||
|
self.me_d2c_batch_bytes_bucket_0_1k
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
1025..=4096 => {
|
||||||
|
self.me_d2c_batch_bytes_bucket_1k_4k
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
4097..=16_384 => {
|
||||||
|
self.me_d2c_batch_bytes_bucket_4k_16k
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
16_385..=65_536 => {
|
||||||
|
self.me_d2c_batch_bytes_bucket_16k_64k
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
65_537..=131_072 => {
|
||||||
|
self.me_d2c_batch_bytes_bucket_64k_128k
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.me_d2c_batch_bytes_bucket_gt_128k
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn observe_me_d2c_flush_duration_us(&self, duration_us: u64) {
|
||||||
|
if !self.telemetry_me_allows_debug() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match duration_us {
|
||||||
|
0..=50 => {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_0_50
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
51..=200 => {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_51_200
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
201..=1000 => {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_201_1000
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
1001..=5000 => {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_1001_5000
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
5001..=20_000 => {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_5001_20000
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_gt_20000
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn increment_me_d2c_batch_timeout_armed_total(&self) {
|
||||||
|
if self.telemetry_me_allows_debug() {
|
||||||
|
self.me_d2c_batch_timeout_armed_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn increment_me_d2c_batch_timeout_fired_total(&self) {
|
||||||
|
if self.telemetry_me_allows_debug() {
|
||||||
|
self.me_d2c_batch_timeout_fired_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn increment_me_writer_pick_success_try_total(&self, mode: MeWriterPickMode) {
|
pub fn increment_me_writer_pick_success_try_total(&self, mode: MeWriterPickMode) {
|
||||||
if !self.telemetry_me_allows_normal() {
|
if !self.telemetry_me_allows_normal() {
|
||||||
return;
|
return;
|
||||||
@@ -1229,6 +1498,142 @@ impl Stats {
|
|||||||
pub fn get_me_route_drop_queue_full_high(&self) -> u64 {
|
pub fn get_me_route_drop_queue_full_high(&self) -> u64 {
|
||||||
self.me_route_drop_queue_full_high.load(Ordering::Relaxed)
|
self.me_route_drop_queue_full_high.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
pub fn get_me_d2c_batches_total(&self) -> u64 {
|
||||||
|
self.me_d2c_batches_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_frames_total(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_frames_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_bytes_total(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_bytes_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_reason_queue_drain_total(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_reason_queue_drain_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_reason_batch_frames_total(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_reason_batch_frames_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_reason_batch_bytes_total(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_reason_batch_bytes_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_reason_max_delay_total(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_reason_max_delay_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_reason_ack_immediate_total(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_reason_ack_immediate_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_reason_close_total(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_reason_close_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_data_frames_total(&self) -> u64 {
|
||||||
|
self.me_d2c_data_frames_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_ack_frames_total(&self) -> u64 {
|
||||||
|
self.me_d2c_ack_frames_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_payload_bytes_total(&self) -> u64 {
|
||||||
|
self.me_d2c_payload_bytes_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_write_mode_coalesced_total(&self) -> u64 {
|
||||||
|
self.me_d2c_write_mode_coalesced_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_write_mode_split_total(&self) -> u64 {
|
||||||
|
self.me_d2c_write_mode_split_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_quota_reject_pre_write_total(&self) -> u64 {
|
||||||
|
self.me_d2c_quota_reject_pre_write_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_quota_reject_post_write_total(&self) -> u64 {
|
||||||
|
self.me_d2c_quota_reject_post_write_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_frame_buf_shrink_total(&self) -> u64 {
|
||||||
|
self.me_d2c_frame_buf_shrink_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_frame_buf_shrink_bytes_total(&self) -> u64 {
|
||||||
|
self.me_d2c_frame_buf_shrink_bytes_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_frames_bucket_1(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_frames_bucket_1.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_frames_bucket_2_4(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_frames_bucket_2_4.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_frames_bucket_5_8(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_frames_bucket_5_8.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_frames_bucket_9_16(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_frames_bucket_9_16.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_frames_bucket_17_32(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_frames_bucket_17_32
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_frames_bucket_gt_32(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_frames_bucket_gt_32
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_bytes_bucket_0_1k(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_bytes_bucket_0_1k.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_bytes_bucket_1k_4k(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_bytes_bucket_1k_4k.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_bytes_bucket_4k_16k(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_bytes_bucket_4k_16k.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_bytes_bucket_16k_64k(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_bytes_bucket_16k_64k
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_bytes_bucket_64k_128k(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_bytes_bucket_64k_128k
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_bytes_bucket_gt_128k(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_bytes_bucket_gt_128k
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_duration_us_bucket_0_50(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_0_50
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_duration_us_bucket_51_200(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_51_200
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_duration_us_bucket_201_1000(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_201_1000
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_duration_us_bucket_1001_5000(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_1001_5000
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_duration_us_bucket_5001_20000(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_5001_20000
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_flush_duration_us_bucket_gt_20000(&self) -> u64 {
|
||||||
|
self.me_d2c_flush_duration_us_bucket_gt_20000
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_timeout_armed_total(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_timeout_armed_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_me_d2c_batch_timeout_fired_total(&self) -> u64 {
|
||||||
|
self.me_d2c_batch_timeout_fired_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
pub fn get_me_writer_pick_sorted_rr_success_try_total(&self) -> u64 {
|
pub fn get_me_writer_pick_sorted_rr_success_try_total(&self) -> u64 {
|
||||||
self.me_writer_pick_sorted_rr_success_try_total
|
self.me_writer_pick_sorted_rr_success_try_total
|
||||||
.load(Ordering::Relaxed)
|
.load(Ordering::Relaxed)
|
||||||
@@ -1898,9 +2303,83 @@ mod tests {
|
|||||||
stats.increment_me_crc_mismatch();
|
stats.increment_me_crc_mismatch();
|
||||||
stats.increment_me_keepalive_sent();
|
stats.increment_me_keepalive_sent();
|
||||||
stats.increment_me_route_drop_queue_full();
|
stats.increment_me_route_drop_queue_full();
|
||||||
|
stats.increment_me_d2c_batches_total();
|
||||||
|
stats.add_me_d2c_batch_frames_total(4);
|
||||||
|
stats.add_me_d2c_batch_bytes_total(4096);
|
||||||
|
stats.increment_me_d2c_flush_reason(MeD2cFlushReason::BatchBytes);
|
||||||
|
stats.increment_me_d2c_write_mode(MeD2cWriteMode::Coalesced);
|
||||||
|
stats.increment_me_d2c_quota_reject_total(MeD2cQuotaRejectStage::PreWrite);
|
||||||
|
stats.observe_me_d2c_frame_buf_shrink(1024);
|
||||||
|
stats.observe_me_d2c_batch_frames(4);
|
||||||
|
stats.observe_me_d2c_batch_bytes(4096);
|
||||||
|
stats.observe_me_d2c_flush_duration_us(120);
|
||||||
|
stats.increment_me_d2c_batch_timeout_armed_total();
|
||||||
|
stats.increment_me_d2c_batch_timeout_fired_total();
|
||||||
assert_eq!(stats.get_me_crc_mismatch(), 0);
|
assert_eq!(stats.get_me_crc_mismatch(), 0);
|
||||||
assert_eq!(stats.get_me_keepalive_sent(), 0);
|
assert_eq!(stats.get_me_keepalive_sent(), 0);
|
||||||
assert_eq!(stats.get_me_route_drop_queue_full(), 0);
|
assert_eq!(stats.get_me_route_drop_queue_full(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_batches_total(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_flush_reason_batch_bytes_total(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_write_mode_coalesced_total(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_quota_reject_pre_write_total(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_frame_buf_shrink_total(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_frames_bucket_2_4(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_bytes_bucket_1k_4k(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_flush_duration_us_bucket_51_200(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_timeout_armed_total(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_timeout_fired_total(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_telemetry_policy_me_normal_blocks_d2c_debug_metrics() {
|
||||||
|
let stats = Stats::new();
|
||||||
|
stats.apply_telemetry_policy(TelemetryPolicy {
|
||||||
|
core_enabled: true,
|
||||||
|
user_enabled: true,
|
||||||
|
me_level: MeTelemetryLevel::Normal,
|
||||||
|
});
|
||||||
|
|
||||||
|
stats.increment_me_d2c_batches_total();
|
||||||
|
stats.add_me_d2c_batch_frames_total(2);
|
||||||
|
stats.add_me_d2c_batch_bytes_total(2048);
|
||||||
|
stats.increment_me_d2c_flush_reason(MeD2cFlushReason::QueueDrain);
|
||||||
|
stats.observe_me_d2c_batch_frames(2);
|
||||||
|
stats.observe_me_d2c_batch_bytes(2048);
|
||||||
|
stats.observe_me_d2c_flush_duration_us(100);
|
||||||
|
stats.increment_me_d2c_batch_timeout_armed_total();
|
||||||
|
stats.increment_me_d2c_batch_timeout_fired_total();
|
||||||
|
|
||||||
|
assert_eq!(stats.get_me_d2c_batches_total(), 1);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_frames_total(), 2);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_bytes_total(), 2048);
|
||||||
|
assert_eq!(stats.get_me_d2c_flush_reason_queue_drain_total(), 1);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_frames_bucket_2_4(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_bytes_bucket_1k_4k(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_flush_duration_us_bucket_51_200(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_timeout_armed_total(), 0);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_timeout_fired_total(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_telemetry_policy_me_debug_enables_d2c_debug_metrics() {
|
||||||
|
let stats = Stats::new();
|
||||||
|
stats.apply_telemetry_policy(TelemetryPolicy {
|
||||||
|
core_enabled: true,
|
||||||
|
user_enabled: true,
|
||||||
|
me_level: MeTelemetryLevel::Debug,
|
||||||
|
});
|
||||||
|
|
||||||
|
stats.observe_me_d2c_batch_frames(7);
|
||||||
|
stats.observe_me_d2c_batch_bytes(70_000);
|
||||||
|
stats.observe_me_d2c_flush_duration_us(1400);
|
||||||
|
stats.increment_me_d2c_batch_timeout_armed_total();
|
||||||
|
stats.increment_me_d2c_batch_timeout_fired_total();
|
||||||
|
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_frames_bucket_5_8(), 1);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_bytes_bucket_64k_128k(), 1);
|
||||||
|
assert_eq!(stats.get_me_d2c_flush_duration_us_bucket_1001_5000(), 1);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_timeout_armed_total(), 1);
|
||||||
|
assert_eq!(stats.get_me_d2c_batch_timeout_fired_total(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -126,14 +126,10 @@ pub(crate) async fn reader_loop(
|
|||||||
let data = body.slice(12..);
|
let data = body.slice(12..);
|
||||||
trace!(cid, flags, len = data.len(), "RPC_PROXY_ANS");
|
trace!(cid, flags, len = data.len(), "RPC_PROXY_ANS");
|
||||||
|
|
||||||
let data_wait_ms = reader_route_data_wait_ms.load(Ordering::Relaxed);
|
let route_wait_ms = reader_route_data_wait_ms.load(Ordering::Relaxed);
|
||||||
let routed = if data_wait_ms == 0 {
|
let routed = reg
|
||||||
reg.route_nowait(cid, MeResponse::Data { flags, data })
|
.route_with_timeout(cid, MeResponse::Data { flags, data }, route_wait_ms)
|
||||||
.await
|
.await;
|
||||||
} else {
|
|
||||||
reg.route_with_timeout(cid, MeResponse::Data { flags, data }, data_wait_ms)
|
|
||||||
.await
|
|
||||||
};
|
|
||||||
if !matches!(routed, RouteResult::Routed) {
|
if !matches!(routed, RouteResult::Routed) {
|
||||||
match routed {
|
match routed {
|
||||||
RouteResult::NoConn => stats.increment_me_route_drop_no_conn(),
|
RouteResult::NoConn => stats.increment_me_route_drop_no_conn(),
|
||||||
|
|||||||
Reference in New Issue
Block a user