mirror of
https://github.com/telemt/telemt.git
synced 2026-06-24 11:51:10 +03:00
Compare commits
326 Commits
f8e1e2f2ea
...
3.3.38
| Author | SHA1 | Date | |
|---|---|---|---|
| e630ea0045 | |||
| 4574e423c6 | |||
| 5f5582865e | |||
| 1f54e4a203 | |||
| defa37da05 | |||
| 5fd058b6fd | |||
| 977ee53b72 | |||
| 5b11522620 | |||
| 8fe6fcb7eb | |||
| 486e439ae6 | |||
| 8e7b27a16d | |||
| 7f0057acd7 | |||
| 7fe38f1b9f | |||
| c2f16a343a | |||
| 6ea867ce36 | |||
| bb6237151c | |||
| a9f695623d | |||
| 5c29870632 | |||
| f6704d7d65 | |||
| 3d20002e56 | |||
| 8fcd0fa950 | |||
| 645e968778 | |||
| b46216d357 | |||
| 8ac1a0017d | |||
| 3df274caa6 | |||
| 780546a680 | |||
| 729ffa0fcd | |||
| e594d6f079 | |||
| ecd6a19246 | |||
| 2df6b8704d | |||
| 5f5a046710 | |||
| 2dc81ad0e0 | |||
| d8d8534cf8 | |||
| 6c850e4150 | |||
| b8cf596e7d | |||
| 5bf56b6dd8 | |||
| 65da1f91ec | |||
| f3e9d00132 | |||
| dee6e13fef | |||
| 07d774a82a | |||
| 618bc7e0b6 | |||
| d06ac222d6 | |||
| 567453e0f8 | |||
| cba837745b | |||
| 876c8f1612 | |||
| ac8ad864be | |||
| fe56dc7c1a | |||
| 96ae01078c | |||
| 3b9919fa4d | |||
| 6c4a3b59f9 | |||
| 01c3d0a707 | |||
| fbee4631d6 | |||
| d0b52ea299 | |||
| 677195e587 | |||
| a383efcb21 | |||
| cb5753f77c | |||
| 7a075b2ffe | |||
| 7de822dd15 | |||
| 1bbf4584a6 | |||
| 70479c4094 | |||
| b94746a6e0 | |||
| ceae1564af | |||
| 7ce5fc66db | |||
| 41493462a1 | |||
| 6ee4d4648c | |||
| 97f6649584 | |||
| dc6b6d3f9d | |||
| 1c3e0d4e46 | |||
| 0b78583cf5 | |||
| 28d318d724 | |||
| 70c2f0f045 | |||
| b9b1271f14 | |||
| 3c734bd811 | |||
| 6391df0583 | |||
| 6a781c8bc3 | |||
| 138652af8e | |||
| 59157d31a6 | |||
| 8bab3f70e1 | |||
| 41d786cc11 | |||
| c43de1bd2a | |||
| 101efe45b7 | |||
| 11df61c6ac | |||
| 08684bcbd2 | |||
| 744fb4425f | |||
| 80cb1bc221 | |||
| 8461556b02 | |||
| cfd516edf3 | |||
| 803c2c0492 | |||
| b762bd029f | |||
| 761679d306 | |||
| 41668b153d | |||
| 1d2f88ad29 | |||
| 80917f5abc | |||
| dc61d300ab | |||
| ae16080de5 | |||
| b8ca1fc166 | |||
| f9986944df | |||
| cb877c2bc3 | |||
| 4426082c17 | |||
| 22097f8c7c | |||
| 1450af60a0 | |||
| f1cc8d65f2 | |||
| ec7e808daf | |||
| e4b7e23e76 | |||
| 8b92b80b4a | |||
| f7868aa00f | |||
| 655a08fa5c | |||
| 8bc432db49 | |||
| a40d6929e5 | |||
| 8db566dbe9 | |||
| bb71de0230 | |||
| 62a258f8e3 | |||
| c868eaae74 | |||
| 8e1860f912 | |||
| 814bef9d99 | |||
| 3ceda15073 | |||
| a3a6ea2880 | |||
| 24156b5067 | |||
| a1dfa5b11d | |||
| 800356c751 | |||
| 1546b012a6 | |||
| e6b77af931 | |||
| 8cfaab9320 | |||
| 2d69b9d0ae | |||
| 41c2b4de65 | |||
| 0a5e8a09fd | |||
| 2f9fddfa6f | |||
| 6f4356f72a | |||
| 0c3c9009a9 | |||
| 0475844701 | |||
| 1abf9bd05c | |||
| 6f17d4d231 | |||
| bf30e93284 | |||
| 91be148b72 | |||
| e46d2cfc52 | |||
| d4cda6d546 | |||
| e35d69c61f | |||
| a353a94175 | |||
| b856250b2c | |||
| 97d1476ded | |||
| cde14fc1bf | |||
| 5723d50d0b | |||
| 3eb384e02a | |||
| c960e0e245 | |||
| 6fc188f0c4 | |||
| 5c9fea5850 | |||
| 3011a9ef6d | |||
| 7b570be5b3 | |||
| 0461bc65c6 | |||
| ead23608f0 | |||
| cf82b637d2 | |||
| 2e8bfa1101 | |||
| d091b0b251 | |||
| 56fc6c4896 | |||
| 042d4fd612 | |||
| bbc69f945e | |||
| 03c9a2588f | |||
| 9de8b2f0bf | |||
| 76eb8634a4 | |||
| 4e5b67bae8 | |||
| bb2f3b24ac | |||
| 73f218b62a | |||
| 9cbc625b9b | |||
| 13ff3af1db | |||
| d3f32b5568 | |||
| 77f717e3d1 | |||
| db3e246390 | |||
| 388e14d01f | |||
| b74ba38d40 | |||
| 269fce839f | |||
| 5a4072c964 | |||
| 95685adba7 | |||
| 909714af31 | |||
| dc2b4395bd | |||
| 39875afbff | |||
| 2ea7813ed4 | |||
| 2d3c2807ab | |||
| 50ae16ddf7 | |||
| de5c26b7d7 | |||
| a95678988a | |||
| b17482ede3 | |||
| a059de9191 | |||
| e7e763888b | |||
| c0a3e43aa8 | |||
| 4c32370b25 | |||
| a6c298b633 | |||
| e7a1d26e6e | |||
| b91c6cb339 | |||
| e676633dcd | |||
| c4e7f54cbe | |||
| f85205d48d | |||
| d767ec02ee | |||
| 51835c33f2 | |||
| 88a4c652b6 | |||
| ea2d964502 | |||
| bd7218c39c | |||
| 3055637571 | |||
| 19b84b9d73 | |||
| 165a1ede57 | |||
| 6ead8b1922 | |||
| 63aa1038c0 | |||
| 4473826303 | |||
| d7bbb376c9 | |||
| 7a8f946029 | |||
| f2e6dc1774 | |||
| 54d65dd124 | |||
| 24594e648e | |||
| e8b38ea860 | |||
| b14c2b0a9b | |||
| c1ee43fbac | |||
| c8632de5b6 | |||
| b930ea1ec5 | |||
| 3b86a883b9 | |||
| 5933b5e821 | |||
| 8188fedf6a | |||
| f3598cf309 | |||
| f2335c211c | |||
| 246ca11b88 | |||
| bb355e916f | |||
| 777b15b1da | |||
| 8814854ae4 | |||
| 44c65f9c60 | |||
| 1260217be9 | |||
| ebd37932c5 | |||
| 43d7e6e991 | |||
| 0eca535955 | |||
| 3abde52de8 | |||
| 801f670827 | |||
| 99ba2f7bbc | |||
| 1689b8a5dc | |||
| babd902d95 | |||
| e14dd07220 | |||
| d93a4fbd53 | |||
| 2798039ab8 | |||
| 9dce748679 | |||
| 79093679ab | |||
| 35a8f5b2e5 | |||
| 456c433875 | |||
| 8f1ffe8c25 | |||
| 342b0119dd | |||
| 2605929b93 | |||
| 36814b6355 | |||
| 269ba537ad | |||
| 5c0eb6dbe8 | |||
| a78c3e3ebd | |||
| a4b70405b8 | |||
| 3afc3e1775 | |||
| 512bee6a8d | |||
| 66867d3f5b | |||
| db36945293 | |||
| 5c5fdcb124 | |||
| 0ded366199 | |||
| 84a34cea3d | |||
| 7dc3c3666d | |||
| dd07fa9453 | |||
| bb1a372ac4 | |||
| 6661401a34 | |||
| cd65fb432b | |||
| caf0717789 | |||
| 4a610d83a3 | |||
| aba4205dcc | |||
| ef9b7b1492 | |||
| d112f15b90 | |||
| b55b264345 | |||
| f61d25ebe0 | |||
| ed4d1167dd | |||
| dc6948cf39 | |||
| 4f11aa0772 | |||
| 6ea8ba25c4 | |||
| e40361b171 | |||
| 1c6c73beda | |||
| 3f3bf5bbd2 | |||
| ec793f3065 | |||
| e83d366518 | |||
| 5a4209fe00 | |||
| e7daf51193 | |||
| 754e4db8a9 | |||
| 7416829e89 | |||
| c07b600acb | |||
| 7b44496706 | |||
| 67dc1e8d18 | |||
| ad8ada33c9 | |||
| bbb201b433 | |||
| 8d1faece60 | |||
| a603505f90 | |||
| f8c42c324f | |||
| dd8ef4d996 | |||
| dc3363aa0d | |||
| f655924323 | |||
| 05c066c676 | |||
| 1e000c2e7e | |||
| fa17e719f6 | |||
| ae3ced8e7c | |||
| 3279f6d46a | |||
| 6f9aef7bb4 | |||
| 049db1196f | |||
| c8ffc23cf7 | |||
| f230f2ce0e | |||
| bdac6e3480 | |||
| a4e9746dc7 | |||
| c47495d671 | |||
| 5ae3a90d5e | |||
| 901a0b7c23 | |||
| 03891db0c9 | |||
| 89e5668c7e | |||
| 1935455256 | |||
| 1544e3fcff | |||
| 85295a9961 | |||
| a54f807a45 | |||
| 31f6258c47 | |||
| 062464175e | |||
| a5983c17d3 | |||
| def42f0baa | |||
| 30ba41eb47 | |||
| 42f946f29e | |||
| c53d7951b5 | |||
| f36e264093 | |||
| a3bdf64353 | |||
| 2aa7ea5137 | |||
| 462c927da6 | |||
| cb87b2eac3 | |||
| 3739f38440 | |||
| 8e96039a1c | |||
| 36b360dfb6 | |||
| 5dd0c47f14 | |||
| 4739083f57 |
@@ -0,0 +1,8 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
target
|
||||||
|
.kilocode
|
||||||
|
cache
|
||||||
|
tlsfront
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# Architecture Directives
|
||||||
|
|
||||||
|
> Companion to `Agents.md`. These are **activation directives**, not tutorials.
|
||||||
|
> You already know these patterns — apply them. When making any structural or
|
||||||
|
> design decision, run the relevant section below as a checklist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Active Principles (always on)
|
||||||
|
|
||||||
|
Apply these on every non-trivial change. No exceptions.
|
||||||
|
|
||||||
|
- **SRP** — one reason to change per component. If you can't name the responsibility in one noun phrase, split it.
|
||||||
|
- **OCP** — extend by adding, not by modifying. New variants/impls over patching existing logic.
|
||||||
|
- **ISP** — traits stay minimal. More than ~5 methods is a split signal.
|
||||||
|
- **DIP** — high-level modules depend on traits, not concrete types. Infrastructure implements domain traits; it does not own domain logic.
|
||||||
|
- **DRY** — one authoritative source per piece of knowledge. Copies are bugs that haven't diverged yet.
|
||||||
|
- **YAGNI** — generic parameters, extension hooks, and pluggable strategies require an *existing* concrete use case, not a hypothetical one.
|
||||||
|
- **KISS** — two equivalent designs: choose the one with fewer concepts. Justify complexity; never assume it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Layered Architecture
|
||||||
|
|
||||||
|
Dependencies point **inward only**: `Presentation → Application → Domain ← Infrastructure`.
|
||||||
|
|
||||||
|
- Domain layer: zero I/O. No network, no filesystem, no async runtime imports.
|
||||||
|
- Infrastructure: implements domain traits at the boundary. Never leaks SDK/wire types inward.
|
||||||
|
- Anti-Corruption Layer (ACL): all third-party and external-protocol types are translated here. If the external format changes, only the ACL changes.
|
||||||
|
- Presentation: translates wire/HTTP representations to domain types and back. Nothing else.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Design Pattern Selection
|
||||||
|
|
||||||
|
Apply the right pattern. Do not invent a new abstraction when a named pattern fits.
|
||||||
|
|
||||||
|
| Situation | Pattern to apply |
|
||||||
|
|---|---|
|
||||||
|
| Struct with 3+ optional/dependent fields | **Builder** — `build()` returns `Result`, never panics |
|
||||||
|
| Cross-cutting behavior (logging, retry, metrics) on a trait impl | **Decorator** — implements same trait, delegates all calls |
|
||||||
|
| Subsystem with multiple internal components | **Façade** — single public entry point, internals are `pub(crate)` |
|
||||||
|
| Swappable algorithm or policy | **Strategy** — trait injection; generics for compile-time, `dyn` for runtime |
|
||||||
|
| Component notifying decoupled consumers | **Observer** — typed channels (`broadcast`, `watch`), not callback `Vec<Box<dyn Fn>>` |
|
||||||
|
| Exclusive mutable state serving concurrent callers | **Actor** — `mpsc` command channel + `oneshot` reply; no lock needed on state |
|
||||||
|
| Finite state with invalid transition prevention | **Typestate** — distinct types per state; invalid ops are compile errors |
|
||||||
|
| Fixed process skeleton with overridable steps | **Template Method** — defaulted trait method calls required hooks |
|
||||||
|
| Request pipeline with independent handlers | **Chain/Middleware** — generic compile-time chain for hot paths, `dyn` for runtime assembly |
|
||||||
|
| Hiding a concrete type behind a trait | **Factory Function** — returns `Box<dyn Trait>` or `impl Trait` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Data Modeling Rules
|
||||||
|
|
||||||
|
- **Make illegal states unrepresentable.** Type system enforces invariants; runtime validation is a second line, not the first.
|
||||||
|
- **Newtype every primitive** that carries domain meaning. `SessionId(u64)` ≠ `UserId(u64)` — the compiler enforces it.
|
||||||
|
- **Enums over booleans** for any parameter or field with two or more named states.
|
||||||
|
- **Typed error enums** with named variants carrying full diagnostic context. `anyhow` is application-layer only; never in library code.
|
||||||
|
- **Domain types carry no I/O concerns.** No `serde`, no codec, no DB derives on domain structs. Conversions via `From`/`TryFrom` at layer boundaries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Concurrency Rules
|
||||||
|
|
||||||
|
- Prefer message-passing over shared memory. Shared state is a fallback.
|
||||||
|
- All channels must be **bounded**. Document the bound's rationale inline.
|
||||||
|
- Never hold a lock across an `await` unless atomicity explicitly requires it — document why.
|
||||||
|
- Document lock acquisition order wherever two locks are taken together.
|
||||||
|
- Every `async fn` is cancellation-safe unless explicitly documented otherwise. Mutate shared state *after* the `await` that may be cancelled, not before.
|
||||||
|
- High-read/low-write state: use `arc-swap` or `watch` for lock-free reads.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Error Handling Rules
|
||||||
|
|
||||||
|
- Errors translated at every layer boundary — low-level errors never surface unmodified.
|
||||||
|
- Add context at the propagation site: what operation failed and where.
|
||||||
|
- No `unwrap()`/`expect()` in production paths without a comment proving `None`/`Err` is impossible.
|
||||||
|
- Panics are only permitted in: tests, startup/init unrecoverable failure, and `unreachable!()` with an invariant comment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. API Design Rules
|
||||||
|
|
||||||
|
- **CQS**: functions that return data must not mutate; functions that mutate return only `Result`.
|
||||||
|
- **Least surprise**: a function does exactly what its name implies. Side effects are documented.
|
||||||
|
- **Idempotency**: `close()`, `shutdown()`, `unregister()` called twice must not panic or error.
|
||||||
|
- **Fallibility at the type level**: failure → `Result<T, E>`. No sentinel values.
|
||||||
|
- **Minimal public surface**: default to `pub(crate)`. Mark `pub` only deliberate API. Re-export through a single surface in `mod.rs`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Performance Rules (hot paths)
|
||||||
|
|
||||||
|
- Annotate hot-path functions with `// HOT PATH: <throughput requirement>`.
|
||||||
|
- Zero allocations per operation in hot paths after initialization. Preallocate in constructors, reuse buffers.
|
||||||
|
- Pass `&[u8]` / `Bytes` slices — not `Vec<u8>`. Use `BytesMut` for reusable mutable buffers.
|
||||||
|
- No `String` formatting in hot paths. No logging without a rate-limit or sampling gate.
|
||||||
|
- Any allocation in a hot path gets a comment: `// ALLOC: <reason and size>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Testing Rules
|
||||||
|
|
||||||
|
- Bug fixes require a regression test that is **red before the fix, green after**. Name it after the bug.
|
||||||
|
- Property tests for: codec round-trips, state machine invariants, cryptographic protocol correctness.
|
||||||
|
- No shared mutable state between tests. Each test constructs its own environment.
|
||||||
|
- Test doubles hierarchy (simplest first): Fake → Stub → Spy → Mock. Mocks couple to implementation, not behavior — use sparingly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Pre-Change Checklist
|
||||||
|
|
||||||
|
Run this before proposing or implementing any structural decision:
|
||||||
|
|
||||||
|
- [ ] Responsibility nameable in one noun phrase?
|
||||||
|
- [ ] Layer dependencies point inward only?
|
||||||
|
- [ ] Invalid states unrepresentable in the type system?
|
||||||
|
- [ ] State transitions gated through a single interface?
|
||||||
|
- [ ] All channels bounded?
|
||||||
|
- [ ] No locks held across `await` (or documented)?
|
||||||
|
- [ ] Errors typed and translated at layer boundaries?
|
||||||
|
- [ ] No panics in production paths without invariant proof?
|
||||||
|
- [ ] Hot paths annotated and allocation-free?
|
||||||
|
- [ ] Public surface minimal — only deliberate API marked `pub`?
|
||||||
|
- [ ] Correct pattern chosen from Section 3 table?
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "*" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "*" ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install latest stable Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache cargo registry & build artifacts
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Build Release
|
||||||
|
run: cargo build --release --verbose
|
||||||
+340
-74
@@ -5,37 +5,87 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- '[0-9]+.[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release tag (example: 3.3.15)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref_name }}-${{ github.event.inputs.tag || 'auto' }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
BINARY_NAME: telemt
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
prepare:
|
||||||
name: Build ${{ matrix.target }}
|
name: Prepare
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
outputs:
|
||||||
|
version: ${{ steps.vars.outputs.version }}
|
||||||
|
prerelease: ${{ steps.vars.outputs.prerelease }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Resolve version
|
||||||
|
id: vars
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||||
|
VERSION="${{ github.event.inputs.tag }}"
|
||||||
|
else
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="${VERSION#refs/tags/}"
|
||||||
|
|
||||||
|
if [ -z "${VERSION}" ]; then
|
||||||
|
echo "Release version is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${VERSION}" == *-* ]]; then
|
||||||
|
PRERELEASE=true
|
||||||
|
else
|
||||||
|
PRERELEASE=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "prerelease=${PRERELEASE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# GNU / glibc
|
||||||
|
# ==========================
|
||||||
|
build-gnu:
|
||||||
|
name: GNU ${{ matrix.asset }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: rust:slim-bookworm
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
artifact_name: telemt
|
asset: telemt-x86_64-linux-gnu
|
||||||
asset_name: telemt-x86_64-linux-gnu
|
cpu: baseline
|
||||||
|
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
asset: telemt-x86_64-v3-linux-gnu
|
||||||
|
cpu: v3
|
||||||
|
|
||||||
- target: aarch64-unknown-linux-gnu
|
- target: aarch64-unknown-linux-gnu
|
||||||
artifact_name: telemt
|
asset: telemt-aarch64-linux-gnu
|
||||||
asset_name: telemt-aarch64-linux-gnu
|
cpu: generic
|
||||||
- target: x86_64-unknown-linux-musl
|
|
||||||
artifact_name: telemt
|
|
||||||
asset_name: telemt-x86_64-linux-musl
|
|
||||||
- target: aarch64-unknown-linux-musl
|
|
||||||
artifact_name: telemt
|
|
||||||
asset_name: telemt-aarch64-linux-musl
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -43,47 +93,245 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
targets: ${{ matrix.target }}
|
targets: |
|
||||||
|
x86_64-unknown-linux-gnu
|
||||||
|
aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
- name: Install cross-compilation tools
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
apt-get update
|
||||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
clang \
|
||||||
|
lld \
|
||||||
|
pkg-config \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
g++-aarch64-linux-gnu
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
/usr/local/cargo/registry
|
||||||
~/.cargo/git
|
/usr/local/cargo/git
|
||||||
target
|
target
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
key: gnu-${{ matrix.asset }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ matrix.target }}-cargo-
|
gnu-${{ matrix.asset }}-
|
||||||
|
gnu-
|
||||||
|
|
||||||
- name: Install cross
|
- name: Build
|
||||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
shell: bash
|
||||||
|
|
||||||
- name: Build Release
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: ${{ contains(matrix.target, 'musl') && '-C target-feature=+crt-static' || '' }}
|
|
||||||
run: cross build --release --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Package binary
|
|
||||||
run: |
|
run: |
|
||||||
cd target/${{ matrix.target }}/release
|
set -euo pipefail
|
||||||
tar -czvf ${{ matrix.asset_name }}.tar.gz ${{ matrix.artifact_name }}
|
|
||||||
sha256sum ${{ matrix.asset_name }}.tar.gz > ${{ matrix.asset_name }}.sha256
|
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
|
||||||
|
export CC=aarch64-linux-gnu-gcc
|
||||||
|
export CXX=aarch64-linux-gnu-g++
|
||||||
|
export RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C lto=fat -C panic=abort"
|
||||||
|
else
|
||||||
|
export CC=clang
|
||||||
|
export CXX=clang++
|
||||||
|
|
||||||
|
if [ "${{ matrix.cpu }}" = "v3" ]; then
|
||||||
|
CPU_FLAGS="-C target-cpu=x86-64-v3"
|
||||||
|
else
|
||||||
|
CPU_FLAGS="-C target-cpu=x86-64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld -C lto=fat -C panic=abort ${CPU_FLAGS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cargo build --release --target ${{ matrix.target }} -j "$(nproc)"
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp "target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}" dist/telemt
|
||||||
|
|
||||||
|
cd dist
|
||||||
|
tar -czf "${{ matrix.asset }}.tar.gz" \
|
||||||
|
--owner=0 --group=0 --numeric-owner \
|
||||||
|
telemt
|
||||||
|
|
||||||
|
sha256sum "${{ matrix.asset }}.tar.gz" > "${{ matrix.asset }}.tar.gz.sha256"
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset_name }}
|
name: ${{ matrix.asset }}
|
||||||
path: |
|
path: dist/*
|
||||||
target/${{ matrix.target }}/release/${{ matrix.asset_name }}.tar.gz
|
|
||||||
target/${{ matrix.target }}/release/${{ matrix.asset_name }}.sha256
|
|
||||||
|
|
||||||
build-docker-image:
|
# ==========================
|
||||||
needs: build
|
# MUSL
|
||||||
|
# ==========================
|
||||||
|
build-musl:
|
||||||
|
name: MUSL ${{ matrix.asset }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: rust:slim-bookworm
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
asset: telemt-x86_64-linux-musl
|
||||||
|
cpu: baseline
|
||||||
|
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
asset: telemt-x86_64-v3-linux-musl
|
||||||
|
cpu: v3
|
||||||
|
|
||||||
|
- target: aarch64-unknown-linux-musl
|
||||||
|
asset: telemt-aarch64-linux-musl
|
||||||
|
cpu: generic
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
musl-tools \
|
||||||
|
pkg-config \
|
||||||
|
curl
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||||
|
with:
|
||||||
|
path: ~/.musl-aarch64
|
||||||
|
key: musl-toolchain-aarch64-v1
|
||||||
|
|
||||||
|
- name: Install aarch64 musl toolchain
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TOOLCHAIN_DIR="$HOME/.musl-aarch64"
|
||||||
|
ARCHIVE="aarch64-linux-musl-cross.tgz"
|
||||||
|
URL="https://github.com/telemt/telemt/releases/download/toolchains/${ARCHIVE}"
|
||||||
|
|
||||||
|
if [ -x "${TOOLCHAIN_DIR}/bin/aarch64-linux-musl-gcc" ]; then
|
||||||
|
echo "MUSL toolchain cached"
|
||||||
|
else
|
||||||
|
curl -fL \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 3 \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--max-time 120 \
|
||||||
|
-o "${ARCHIVE}" "${URL}"
|
||||||
|
|
||||||
|
mkdir -p "${TOOLCHAIN_DIR}"
|
||||||
|
tar -xzf "${ARCHIVE}" --strip-components=1 -C "${TOOLCHAIN_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${TOOLCHAIN_DIR}/bin" >> "${GITHUB_PATH}"
|
||||||
|
|
||||||
|
- name: Add rust target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/usr/local/cargo/registry
|
||||||
|
/usr/local/cargo/git
|
||||||
|
target
|
||||||
|
key: musl-${{ matrix.asset }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
musl-${{ matrix.asset }}-
|
||||||
|
musl-
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
|
||||||
|
export CC=aarch64-linux-musl-gcc
|
||||||
|
export CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc
|
||||||
|
export RUSTFLAGS="-C target-feature=+crt-static -C linker=aarch64-linux-musl-gcc -C lto=fat -C panic=abort"
|
||||||
|
else
|
||||||
|
export CC=musl-gcc
|
||||||
|
export CC_x86_64_unknown_linux_musl=musl-gcc
|
||||||
|
|
||||||
|
if [ "${{ matrix.cpu }}" = "v3" ]; then
|
||||||
|
CPU_FLAGS="-C target-cpu=x86-64-v3"
|
||||||
|
else
|
||||||
|
CPU_FLAGS="-C target-cpu=x86-64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export RUSTFLAGS="-C target-feature=+crt-static -C lto=fat -C panic=abort ${CPU_FLAGS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cargo build --release --target ${{ matrix.target }} -j "$(nproc)"
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp "target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}" dist/telemt
|
||||||
|
|
||||||
|
cd dist
|
||||||
|
tar -czf "${{ matrix.asset }}.tar.gz" \
|
||||||
|
--owner=0 --group=0 --numeric-owner \
|
||||||
|
telemt
|
||||||
|
|
||||||
|
sha256sum "${{ matrix.asset }}.tar.gz" > "${{ matrix.asset }}.tar.gz.sha256"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.asset }}
|
||||||
|
path: dist/*
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Release
|
||||||
|
# ==========================
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [prepare, build-gnu, build-musl]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Flatten artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
find artifacts -type f -exec cp {} dist/ \;
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ needs.prepare.outputs.version }}
|
||||||
|
target_commitish: ${{ github.sha }}
|
||||||
|
files: dist/*
|
||||||
|
generate_release_notes: true
|
||||||
|
prerelease: ${{ needs.prepare.outputs.prerelease == 'true' }}
|
||||||
|
overwrite_files: true
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Docker
|
||||||
|
# ==========================
|
||||||
|
docker:
|
||||||
|
name: Docker
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [prepare, release]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -92,48 +340,66 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v3
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to GHCR
|
- uses: docker/login-action@v3
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract version
|
- name: Probe release assets
|
||||||
id: vars
|
shell: bash
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
env:
|
||||||
|
VERSION: ${{ needs.prepare.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
- name: Build and push
|
for asset in \
|
||||||
|
telemt-x86_64-linux-musl.tar.gz \
|
||||||
|
telemt-x86_64-linux-musl.tar.gz.sha256 \
|
||||||
|
telemt-aarch64-linux-musl.tar.gz \
|
||||||
|
telemt-aarch64-linux-musl.tar.gz.sha256
|
||||||
|
do
|
||||||
|
curl -fsIL \
|
||||||
|
--retry 10 \
|
||||||
|
--retry-delay 3 \
|
||||||
|
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${VERSION}/${asset}" \
|
||||||
|
> /dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Compute image tags
|
||||||
|
id: meta
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.prepare.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE="$(echo "ghcr.io/${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
TAGS="${IMAGE}:${VERSION}"
|
||||||
|
|
||||||
|
if [[ "${VERSION}" != *-* ]]; then
|
||||||
|
TAGS="${TAGS}"$'\n'"${IMAGE}:latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "tags<<EOF"
|
||||||
|
printf '%s\n' "${TAGS}"
|
||||||
|
echo "EOF"
|
||||||
|
} >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Build & Push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
pull: true
|
||||||
ghcr.io/${{ github.repository }}:${{ steps.vars.outputs.VERSION }}
|
platforms: linux/amd64,linux/arm64
|
||||||
ghcr.io/${{ github.repository }}:latest
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
build-args: |
|
||||||
release:
|
TELEMT_REPOSITORY=${{ github.repository }}
|
||||||
name: Create Release
|
TELEMT_VERSION=${{ needs.prepare.outputs.version }}
|
||||||
needs: build
|
cache-from: type=gha
|
||||||
runs-on: ubuntu-latest
|
cache-to: type=gha,mode=max
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: artifacts
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: artifacts/**/*
|
|
||||||
generate_release_notes: true
|
|
||||||
draft: false
|
|
||||||
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
name: Rust
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "*" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "*" ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
checks: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install latest stable Rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: rustfmt, clippy
|
|
||||||
|
|
||||||
- name: Cache cargo registry & build artifacts
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- name: Build Release
|
|
||||||
run: cargo build --release --verbose
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --verbose
|
|
||||||
|
|
||||||
# clippy dont fail on warnings because of active development of telemt
|
|
||||||
# and many warnings
|
|
||||||
- name: Run clippy
|
|
||||||
run: cargo clippy -- --cap-lints warn
|
|
||||||
|
|
||||||
- name: Check for unused dependencies
|
|
||||||
run: cargo udeps || true
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
name: Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "*" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "*" ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: test-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ==========================
|
||||||
|
# Formatting
|
||||||
|
# ==========================
|
||||||
|
fmt:
|
||||||
|
name: Fmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt
|
||||||
|
|
||||||
|
- run: cargo fmt -- --check
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Tests
|
||||||
|
# ==========================
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
checks: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache cargo
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-nextest-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-nextest-
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Install cargo-nextest
|
||||||
|
run: cargo install --locked cargo-nextest || true
|
||||||
|
|
||||||
|
- name: Run tests with nextest
|
||||||
|
run: cargo nextest run -j "$(nproc)"
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Clippy
|
||||||
|
# ==========================
|
||||||
|
clippy:
|
||||||
|
name: Clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
checks: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: clippy
|
||||||
|
|
||||||
|
- name: Cache cargo
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-clippy-
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy -j "$(nproc)" -- --cap-lints warn
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Udeps
|
||||||
|
# ==========================
|
||||||
|
udeps:
|
||||||
|
name: Udeps
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rust-src
|
||||||
|
|
||||||
|
- name: Cache cargo
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-udeps-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-udeps-
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Install cargo-udeps
|
||||||
|
run: cargo install --locked cargo-udeps || true
|
||||||
|
|
||||||
|
- name: Run udeps
|
||||||
|
run: cargo udeps -j "$(nproc)" || true
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# Architect Mode Rules for Telemt
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph Entry
|
|
||||||
Client[Clients] --> Listener[TCP/Unix Listener]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Proxy Layer
|
|
||||||
Listener --> ClientHandler[ClientHandler]
|
|
||||||
ClientHandler --> Handshake[Handshake Validator]
|
|
||||||
Handshake --> |Valid| Relay[Relay Layer]
|
|
||||||
Handshake --> |Invalid| Masking[Masking/TLS Fronting]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Transport
|
|
||||||
Relay --> MiddleProxy[Middle-End Proxy Pool]
|
|
||||||
Relay --> DirectRelay[Direct DC Relay]
|
|
||||||
MiddleProxy --> TelegramDC[Telegram DCs]
|
|
||||||
DirectRelay --> TelegramDC
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module Dependencies
|
|
||||||
- [`src/main.rs`](src/main.rs) - Entry point, spawns all async tasks
|
|
||||||
- [`src/config/`](src/config/) - Configuration loading with auto-migration
|
|
||||||
- [`src/error.rs`](src/error.rs) - Error types, must be used by all modules
|
|
||||||
- [`src/crypto/`](src/crypto/) - AES, SHA, random number generation
|
|
||||||
- [`src/protocol/`](src/protocol/) - MTProto constants, frame encoding, obfuscation
|
|
||||||
- [`src/stream/`](src/stream/) - Stream wrappers, buffer pool, frame codecs
|
|
||||||
- [`src/proxy/`](src/proxy/) - Client handling, handshake, relay logic
|
|
||||||
- [`src/transport/`](src/transport/) - Upstream management, middle-proxy, SOCKS support
|
|
||||||
- [`src/stats/`](src/stats/) - Statistics and replay protection
|
|
||||||
- [`src/ip_tracker.rs`](src/ip_tracker.rs) - Per-user IP tracking
|
|
||||||
|
|
||||||
## Key Architectural Constraints
|
|
||||||
|
|
||||||
### Middle-End Proxy Mode
|
|
||||||
- Requires public IP on interface OR 1:1 NAT with STUN probing
|
|
||||||
- Uses separate `proxy-secret` from Telegram (NOT user secrets)
|
|
||||||
- Falls back to direct mode automatically on STUN mismatch
|
|
||||||
|
|
||||||
### TLS Fronting
|
|
||||||
- Invalid handshakes are transparently proxied to `mask_host`
|
|
||||||
- This is critical for DPI evasion - do not change this behavior
|
|
||||||
- `mask_unix_sock` and `mask_host` are mutually exclusive
|
|
||||||
|
|
||||||
### Stream Architecture
|
|
||||||
- Buffer pool is shared globally via Arc - prevents allocation storms
|
|
||||||
- Frame codecs implement tokio-util Encoder/Decoder traits
|
|
||||||
- State machine in [`src/stream/state.rs`](src/stream/state.rs) manages stream transitions
|
|
||||||
|
|
||||||
### Configuration Migration
|
|
||||||
- [`ProxyConfig::load()`](src/config/mod.rs:641) mutates config in-place
|
|
||||||
- New fields must have sensible defaults
|
|
||||||
- DC203 override is auto-injected for CDN/media support
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Code Mode Rules for Telemt
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
- Always use [`ProxyError`](src/error.rs:168) from [`src/error.rs`](src/error.rs) for proxy operations
|
|
||||||
- [`HandshakeResult<T,R,W>`](src/error.rs:292) returns streams on bad client - these MUST be returned for masking, never dropped
|
|
||||||
- Use [`Recoverable`](src/error.rs:110) trait to check if errors are retryable
|
|
||||||
|
|
||||||
## Configuration Changes
|
|
||||||
- [`ProxyConfig::load()`](src/config/mod.rs:641) auto-mutates config - new fields should have defaults
|
|
||||||
- DC203 override is auto-injected if missing - do not remove this behavior
|
|
||||||
- When adding config fields, add migration logic in [`ProxyConfig::load()`](src/config/mod.rs:641)
|
|
||||||
|
|
||||||
## Crypto Code
|
|
||||||
- [`SecureRandom`](src/crypto/random.rs) from [`src/crypto/random.rs`](src/crypto/random.rs) must be used for all crypto operations
|
|
||||||
- Never use `rand::thread_rng()` directly - use the shared `Arc<SecureRandom>`
|
|
||||||
|
|
||||||
## Stream Handling
|
|
||||||
- Buffer pool [`BufferPool`](src/stream/buffer_pool.rs) is shared via Arc - always use it instead of allocating
|
|
||||||
- Frame codecs in [`src/stream/frame_codec.rs`](src/stream/frame_codec.rs) implement tokio-util's Encoder/Decoder traits
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
- Tests are inline in modules using `#[cfg(test)]`
|
|
||||||
- Use `cargo test --lib <module_name>` to run tests for specific modules
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Debug Mode Rules for Telemt
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
- `RUST_LOG` environment variable takes absolute priority over all config log levels
|
|
||||||
- Log levels: `trace`, `debug`, `info`, `warn`, `error`
|
|
||||||
- Use `RUST_LOG=debug cargo run` for detailed operational logs
|
|
||||||
- Use `RUST_LOG=trace cargo run` for full protocol-level debugging
|
|
||||||
|
|
||||||
## Middle-End Proxy Debugging
|
|
||||||
- Set `ME_DIAG=1` environment variable for high-precision cryptography diagnostics
|
|
||||||
- STUN probe results are logged at startup - check for mismatch between local and reflected IP
|
|
||||||
- If Middle-End fails, check `proxy_secret_path` points to valid file from https://core.telegram.org/getProxySecret
|
|
||||||
|
|
||||||
## Connection Issues
|
|
||||||
- DC connectivity is logged at startup with RTT measurements
|
|
||||||
- If DC ping fails, check `dc_overrides` for custom addresses
|
|
||||||
- Use `prefer_ipv6=false` in config if IPv6 is unreliable
|
|
||||||
|
|
||||||
## TLS Fronting Issues
|
|
||||||
- Invalid handshakes are proxied to `mask_host` - check this host is reachable
|
|
||||||
- `mask_unix_sock` and `mask_host` are mutually exclusive - only one can be set
|
|
||||||
- If `mask_unix_sock` is set, socket must exist before connections arrive
|
|
||||||
|
|
||||||
## Common Errors
|
|
||||||
- `ReplayAttack` - client replayed a handshake nonce, potential attack
|
|
||||||
- `TimeSkew` - client clock is off, can disable with `ignore_time_skew=true`
|
|
||||||
- `TgHandshakeTimeout` - upstream DC connection failed, check network
|
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
**Telemt exists to solve technical problems.**
|
||||||
|
|
||||||
|
Telemt is open to contributors who want to learn, improve and build meaningful systems together.
|
||||||
|
|
||||||
|
It is a place for building, testing, reasoning, documenting, and improving systems.
|
||||||
|
|
||||||
|
Discussions that advance this work are in scope. Discussions that divert it are not.
|
||||||
|
|
||||||
|
Technology has consequences. Responsibility is inherent.
|
||||||
|
|
||||||
|
> **Zweck bestimmt die Form.**
|
||||||
|
|
||||||
|
> Purpose defines form.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
* **Technical over emotional**
|
||||||
|
|
||||||
|
Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
|
||||||
|
|
||||||
|
* **Clarity over noise**
|
||||||
|
|
||||||
|
Communication is structured, concise, and relevant.
|
||||||
|
|
||||||
|
* **Openness with standards**
|
||||||
|
|
||||||
|
Participation is open. The work remains disciplined.
|
||||||
|
|
||||||
|
* **Independence of judgment**
|
||||||
|
|
||||||
|
Claims are evaluated on technical merit, not affiliation or posture.
|
||||||
|
|
||||||
|
* **Responsibility over capability**
|
||||||
|
|
||||||
|
Capability does not justify careless use.
|
||||||
|
|
||||||
|
* **Cooperation over friction**
|
||||||
|
|
||||||
|
Progress depends on coordination, mutual support, and honest review.
|
||||||
|
|
||||||
|
* **Good intent, rigorous method**
|
||||||
|
|
||||||
|
Assume good intent, but require rigor.
|
||||||
|
|
||||||
|
> **Aussagen gelten nach ihrer Begründung.**
|
||||||
|
|
||||||
|
> Claims are weighed by evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Participants are expected to:
|
||||||
|
|
||||||
|
* Communicate directly and respectfully
|
||||||
|
* Support claims with evidence
|
||||||
|
* Stay within technical scope
|
||||||
|
* Accept critique and provide it constructively
|
||||||
|
* Reduce noise, duplication, and ambiguity
|
||||||
|
* Help others reach correct and reproducible outcomes
|
||||||
|
* Act in a way that improves the system as a whole
|
||||||
|
|
||||||
|
Precision is learned.
|
||||||
|
|
||||||
|
New contributors are welcome. They are expected to grow into these standards. Existing contributors are expected to make that growth possible.
|
||||||
|
|
||||||
|
> **Wer behauptet, belegt.**
|
||||||
|
|
||||||
|
> Whoever claims, proves.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unacceptable Behavior
|
||||||
|
|
||||||
|
The following is not allowed:
|
||||||
|
|
||||||
|
* Personal attacks, insults, harassment, or intimidation
|
||||||
|
* Repeatedly derailing discussion away from Telemt’s purpose
|
||||||
|
* Spam, flooding, or repeated low-quality input
|
||||||
|
* Misinformation presented as fact
|
||||||
|
* Attempts to degrade, destabilize, or exhaust Telemt or its participants
|
||||||
|
* Use of Telemt or its spaces to enable harm
|
||||||
|
|
||||||
|
Telemt is not a venue for disputes that displace technical work.
|
||||||
|
Such discussions may be closed, removed, or redirected.
|
||||||
|
|
||||||
|
> **Störung ist kein Beitrag.**
|
||||||
|
|
||||||
|
> Disruption is not contribution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security and Misuse
|
||||||
|
|
||||||
|
Telemt is intended for responsible use.
|
||||||
|
|
||||||
|
* Do not use it to plan, coordinate, or execute harm
|
||||||
|
* Do not publish vulnerabilities without responsible disclosure
|
||||||
|
* Report security issues privately where possible
|
||||||
|
|
||||||
|
Security is both technical and behavioral.
|
||||||
|
|
||||||
|
> **Verantwortung endet nicht am Code.**
|
||||||
|
|
||||||
|
> Responsibility does not end at the code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Openness
|
||||||
|
|
||||||
|
Telemt is open to contributors of different backgrounds, experience levels, and working styles.
|
||||||
|
|
||||||
|
- Standards are public, legible, and applied to the work itself.
|
||||||
|
- Questions are welcome. Careful disagreement is welcome. Honest correction is welcome.
|
||||||
|
- Gatekeeping by obscurity, status signaling, or hostility is not.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies to all official spaces:
|
||||||
|
|
||||||
|
* Source repositories (issues, pull requests, discussions)
|
||||||
|
* Documentation
|
||||||
|
* Communication channels associated with Telemt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Maintainer Stewardship
|
||||||
|
|
||||||
|
Maintainers are responsible for final decisions in matters of conduct, scope, and direction.
|
||||||
|
|
||||||
|
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.
|
||||||
|
- Not every decision requires extended debate.
|
||||||
|
- Not every intervention requires public explanation.
|
||||||
|
|
||||||
|
All decisions are expected to serve the durability, clarity, and integrity of Telemt.
|
||||||
|
|
||||||
|
> **Ordnung ist Voraussetzung der Funktion.**
|
||||||
|
|
||||||
|
> Order is the precondition of function.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Maintainers may act to preserve the integrity of Telemt, including by:
|
||||||
|
|
||||||
|
* Removing content
|
||||||
|
* Locking discussions
|
||||||
|
* Rejecting contributions
|
||||||
|
* Restricting or banning participants
|
||||||
|
|
||||||
|
Actions are taken to maintain function, continuity, and signal quality.
|
||||||
|
- Where possible, correction is preferred to exclusion.
|
||||||
|
- Where necessary, exclusion is preferred to decay.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final
|
||||||
|
|
||||||
|
Telemt is built on discipline, structure, and shared intent.
|
||||||
|
- Signal over noise.
|
||||||
|
- Facts over opinion.
|
||||||
|
- Systems over rhetoric.
|
||||||
|
|
||||||
|
- Work is collective.
|
||||||
|
- Outcomes are shared.
|
||||||
|
- Responsibility is distributed.
|
||||||
|
|
||||||
|
- Precision is learned.
|
||||||
|
- Rigor is expected.
|
||||||
|
- Help is part of the work.
|
||||||
|
|
||||||
|
> **Ordnung ist Voraussetzung der Freiheit.**
|
||||||
|
|
||||||
|
- If you contribute — contribute with care.
|
||||||
|
- If you speak — speak with substance.
|
||||||
|
- If you engage — engage constructively.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After All
|
||||||
|
|
||||||
|
Systems outlive intentions.
|
||||||
|
- What is built will be used.
|
||||||
|
- What is released will propagate.
|
||||||
|
- What is maintained will define the future state.
|
||||||
|
|
||||||
|
There is no neutral infrastructure, only infrastructure shaped well or poorly.
|
||||||
|
|
||||||
|
> **Jedes System trägt Verantwortung.**
|
||||||
|
|
||||||
|
> Every system carries responsibility.
|
||||||
|
|
||||||
|
- Stability requires discipline.
|
||||||
|
- Freedom requires structure.
|
||||||
|
- Trust requires honesty.
|
||||||
|
|
||||||
|
In the end: the system reflects its contributors.
|
||||||
+72
-9
@@ -1,19 +1,82 @@
|
|||||||
# Issues - Rules
|
# Issues
|
||||||
|
## Warnung
|
||||||
|
Before opening Issue, if it is more question than problem or bug - ask about that [in our chat](https://t.me/telemtrs)
|
||||||
|
|
||||||
## What it is not
|
## What it is not
|
||||||
- NOT Question and Answer
|
- NOT Question and Answer
|
||||||
- NOT Helpdesk
|
- NOT Helpdesk
|
||||||
|
|
||||||
# Pull Requests - Rules
|
***Each of your Issues triggers attempts to reproduce problems and analyze them, which are done manually by people***
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pull Requests
|
||||||
|
|
||||||
## General
|
## General
|
||||||
- ONLY signed and verified commits
|
- ONLY signed and verified commits
|
||||||
- ONLY from your name
|
- ONLY from your name
|
||||||
- DO NOT commit with `codex` or `claude` as author/commiter
|
- DO NOT commit with `codex`, `claude`, or other AI tools as author/committer
|
||||||
- PREFER `flow` branch for development, not `main`
|
- PREFER `flow` branch for development, not `main`
|
||||||
|
|
||||||
## AI
|
---
|
||||||
We are not against modern tools, like AI, where you act as a principal or architect, but we consider it important:
|
|
||||||
|
|
||||||
- you really understand what you're doing
|
## Definition of Ready (MANDATORY)
|
||||||
- you understand the relationships and dependencies of the components being modified
|
|
||||||
- you understand the architecture of Telegram MTProto, MTProxy, Middle-End KDF at least generically
|
A Pull Request WILL be ignored or closed if:
|
||||||
- you DO NOT commit for the sake of commits, but to help the community, core-developers and ordinary users
|
|
||||||
|
- it does NOT build
|
||||||
|
- it does NOT pass tests
|
||||||
|
- it does NOT follow formatting rules
|
||||||
|
- it contains unrelated or excessive changes
|
||||||
|
- the author cannot clearly explain the change
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blessed Principles
|
||||||
|
- PR must build
|
||||||
|
- PR must pass tests
|
||||||
|
- PR must be understood by author
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI Usage Policy
|
||||||
|
|
||||||
|
AI tools (Claude, ChatGPT, Codex, DeepSeek, etc.) are allowed as **assistants**, NOT as decision-makers.
|
||||||
|
|
||||||
|
By submitting a PR, you confirm that:
|
||||||
|
|
||||||
|
- you fully understand the code you submit
|
||||||
|
- you verified correctness manually
|
||||||
|
- you reviewed architecture and dependencies
|
||||||
|
- you take full responsibility for the change
|
||||||
|
|
||||||
|
AI-generated code is treated as **draft** and must be validated like any other external contribution.
|
||||||
|
|
||||||
|
PRs that look like unverified AI dumps WILL be closed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Maintainer Policy
|
||||||
|
|
||||||
|
Maintainers reserve the right to:
|
||||||
|
|
||||||
|
- close PRs that do not meet basic quality requirements
|
||||||
|
- request explanations before review
|
||||||
|
- ignore low-effort contributions
|
||||||
|
|
||||||
|
Respect the reviewers time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Pull Requests that violate project standards may be closed without review.
|
||||||
|
|
||||||
|
This includes (but is not limited to):
|
||||||
|
|
||||||
|
- non-building code
|
||||||
|
- failing tests
|
||||||
|
- unverified or low-effort changes
|
||||||
|
- inability to explain the change
|
||||||
|
|
||||||
|
These actions follow the Code of Conduct and are intended to preserve signal, quality, and Telemt's integrity
|
||||||
Generated
+1164
-421
File diff suppressed because it is too large
Load Diff
+35
-15
@@ -1,8 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.3.20"
|
version = "3.3.38"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
redteam_offline_expected_fail = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# C
|
# C
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
@@ -26,24 +29,33 @@ subtle = "2.6"
|
|||||||
static_assertions = "1.1"
|
static_assertions = "1.1"
|
||||||
|
|
||||||
# Network
|
# Network
|
||||||
socket2 = { version = "0.5", features = ["all"] }
|
socket2 = { version = "0.6", features = ["all"] }
|
||||||
nix = { version = "0.28", default-features = false, features = ["net"] }
|
nix = { version = "0.31", default-features = false, features = [
|
||||||
|
"net",
|
||||||
|
"user",
|
||||||
|
"process",
|
||||||
|
"fs",
|
||||||
|
"signal",
|
||||||
|
] }
|
||||||
|
shadowsocks = { version = "1.24", features = ["aead-cipher-2022"] }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = "0.8"
|
toml = "1.0"
|
||||||
x509-parser = "0.15"
|
x509-parser = "0.18"
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
bytes = "1.9"
|
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 = "5.5"
|
dashmap = "6.1"
|
||||||
|
arc-swap = "1.7"
|
||||||
lru = "0.16"
|
lru = "0.16"
|
||||||
rand = "0.9"
|
rand = "0.10"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
@@ -56,20 +68,26 @@ x25519-dalek = "2"
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
||||||
# HTTP
|
# HTTP
|
||||||
reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false }
|
reqwest = { version = "0.13", features = ["rustls"], default-features = false }
|
||||||
notify = { version = "6", features = ["macos_fsevent"] }
|
notify = "8.2"
|
||||||
ipnetwork = "0.20"
|
ipnetwork = { version = "0.21", features = ["serde"] }
|
||||||
hyper = { version = "1", features = ["server", "http1"] }
|
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",
|
||||||
webpki-roots = "0.26"
|
] }
|
||||||
|
rustls = { version = "0.23", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"tls12",
|
||||||
|
"ring",
|
||||||
|
] }
|
||||||
|
webpki-roots = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
criterion = "0.5"
|
criterion = "0.8"
|
||||||
proptest = "1.4"
|
proptest = "1.4"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
|
||||||
@@ -78,4 +96,6 @@ name = "crypto_bench"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
|
|||||||
+84
-29
@@ -1,43 +1,98 @@
|
|||||||
# ==========================
|
# syntax=docker/dockerfile:1
|
||||||
# Stage 1: Build
|
|
||||||
# ==========================
|
|
||||||
FROM rust:1.88-slim-bookworm AS builder
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
ARG TELEMT_REPOSITORY=telemt/telemt
|
||||||
pkg-config \
|
ARG TELEMT_VERSION=latest
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
COPY Cargo.toml Cargo.lock* ./
|
|
||||||
RUN mkdir src && echo 'fn main() {}' > src/main.rs && \
|
|
||||||
cargo build --release 2>/dev/null || true && \
|
|
||||||
rm -rf src
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN cargo build --release && strip target/release/telemt
|
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Stage 2: Runtime
|
# Minimal Image
|
||||||
# ==========================
|
# ==========================
|
||||||
FROM debian:bookworm-slim
|
FROM debian:12-slim AS minimal
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
ARG TARGETARCH
|
||||||
ca-certificates \
|
ARG TELEMT_REPOSITORY
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
ARG TELEMT_VERSION
|
||||||
|
|
||||||
RUN useradd -r -s /usr/sbin/nologin telemt
|
RUN set -eux; \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
binutils \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
tar; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
case "${TARGETARCH}" in \
|
||||||
|
amd64) ASSET="telemt-x86_64-linux-musl.tar.gz" ;; \
|
||||||
|
arm64) ASSET="telemt-aarch64-linux-musl.tar.gz" ;; \
|
||||||
|
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
VERSION="${TELEMT_VERSION#refs/tags/}"; \
|
||||||
|
if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then \
|
||||||
|
BASE_URL="https://github.com/${TELEMT_REPOSITORY}/releases/latest/download"; \
|
||||||
|
else \
|
||||||
|
BASE_URL="https://github.com/${TELEMT_REPOSITORY}/releases/download/${VERSION}"; \
|
||||||
|
fi; \
|
||||||
|
curl -fL \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 3 \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--max-time 120 \
|
||||||
|
-o "/tmp/${ASSET}" \
|
||||||
|
"${BASE_URL}/${ASSET}"; \
|
||||||
|
curl -fL \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 3 \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--max-time 120 \
|
||||||
|
-o "/tmp/${ASSET}.sha256" \
|
||||||
|
"${BASE_URL}/${ASSET}.sha256"; \
|
||||||
|
cd /tmp; \
|
||||||
|
sha256sum -c "${ASSET}.sha256"; \
|
||||||
|
tar -xzf "${ASSET}" -C /tmp; \
|
||||||
|
test -f /tmp/telemt; \
|
||||||
|
install -m 0755 /tmp/telemt /telemt; \
|
||||||
|
strip --strip-unneeded /telemt || true; \
|
||||||
|
rm -f "/tmp/${ASSET}" "/tmp/${ASSET}.sha256" /tmp/telemt
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Debug Image
|
||||||
|
# ==========================
|
||||||
|
FROM debian:12-slim AS debug
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
curl \
|
||||||
|
iproute2 \
|
||||||
|
busybox; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/telemt /app/telemt
|
COPY --from=minimal /telemt /app/telemt
|
||||||
COPY config.toml /app/config.toml
|
COPY config.toml /app/config.toml
|
||||||
|
|
||||||
RUN chown -R telemt:telemt /app
|
EXPOSE 443 9090 9091
|
||||||
USER telemt
|
|
||||||
|
ENTRYPOINT ["/app/telemt"]
|
||||||
EXPOSE 443
|
CMD ["config.toml"]
|
||||||
EXPOSE 9090
|
|
||||||
|
# ==========================
|
||||||
|
# Production Distroless on MUSL
|
||||||
|
# ==========================
|
||||||
|
FROM gcr.io/distroless/static-debian12 AS prod
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=minimal /telemt /app/telemt
|
||||||
|
COPY config.toml /app/config.toml
|
||||||
|
|
||||||
|
USER nonroot:nonroot
|
||||||
|
|
||||||
|
EXPOSE 443 9090 9091
|
||||||
|
|
||||||
ENTRYPOINT ["/app/telemt"]
|
ENTRYPOINT ["/app/telemt"]
|
||||||
CMD ["config.toml"]
|
CMD ["config.toml"]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
***Löst Probleme, bevor andere überhaupt wissen, dass sie existieren*** / ***It solves problems before others even realize they exist***
|
***Löst Probleme, bevor andere überhaupt wissen, dass sie existieren*** / ***It solves problems before others even realize they exist***
|
||||||
|
|
||||||
|
### [**Telemt Chat in Telegram**](https://t.me/telemtrs)
|
||||||
|
#### Fixed TLS ClientHello is now available in Telegram Desktop starting from version 6.7.2: to work with EE-MTProxy, please update your client;
|
||||||
|
#### Fixed TLS ClientHello for Telegram Android Client is available in [our chat](https://t.me/telemtrs/30234/36441); official releases for Android and iOS are "work in progress";
|
||||||
|
|
||||||
|
|
||||||
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as:
|
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as:
|
||||||
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/model/MODEL.en.md)
|
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/model/MODEL.en.md)
|
||||||
- [Full-covered API w/ management](https://github.com/telemt/telemt/blob/main/docs/API.md)
|
- [Full-covered API w/ management](https://github.com/telemt/telemt/blob/main/docs/API.md)
|
||||||
@@ -9,60 +14,6 @@
|
|||||||
- Prometheus-format Metrics
|
- Prometheus-format Metrics
|
||||||
- TLS-Fronting and TCP-Splicing for masking from "prying" eyes
|
- TLS-Fronting and TCP-Splicing for masking from "prying" eyes
|
||||||
|
|
||||||
[**Telemt Chat in Telegram**](https://t.me/telemtrs)
|
|
||||||
|
|
||||||
## NEWS and EMERGENCY
|
|
||||||
### ✈️ Telemt 3 is released!
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td width="50%" valign="top">
|
|
||||||
|
|
||||||
### 🇷🇺 RU
|
|
||||||
|
|
||||||
#### Релиз 3.3.15 Semistable
|
|
||||||
|
|
||||||
[3.3.15](https://github.com/telemt/telemt/releases/tag/3.3.15) по итогам работы в продакшн признан одним из самых стабильных и рекомендуется к использованию, когда cutting-edge фичи некритичны!
|
|
||||||
|
|
||||||
Будем рады вашему фидбеку и предложениям по улучшению — особенно в части **API**, **статистики**, **UX**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Если у вас есть компетенции в:
|
|
||||||
|
|
||||||
- Асинхронных сетевых приложениях
|
|
||||||
- Анализе трафика
|
|
||||||
- Реверс-инжиниринге
|
|
||||||
- Сетевых расследованиях
|
|
||||||
|
|
||||||
Мы открыты к архитектурным предложениям, идеям и pull requests
|
|
||||||
</td>
|
|
||||||
<td width="50%" valign="top">
|
|
||||||
|
|
||||||
### 🇬🇧 EN
|
|
||||||
|
|
||||||
#### Release 3.3.15 Semistable
|
|
||||||
|
|
||||||
[3.3.15](https://github.com/telemt/telemt/releases/tag/3.3.15) is, based on the results of his work in production, recognized as one of the most stable and recommended for use when cutting-edge features are not so necessary!
|
|
||||||
|
|
||||||
We are looking forward to your feedback and improvement proposals — especially regarding **API**, **statistics**, **UX**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
If you have expertise in:
|
|
||||||
|
|
||||||
- Asynchronous network applications
|
|
||||||
- Traffic analysis
|
|
||||||
- Reverse engineering
|
|
||||||
- Network forensics
|
|
||||||
|
|
||||||
We welcome ideas, architectural feedback, and pull requests.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
# Features
|
|
||||||
💥 The configuration structure has changed since version 1.1.0.0. change it in your environment!
|
|
||||||
|
|
||||||
⚓ Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](#recognizability-for-dpi-and-crawler)
|
⚓ Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](#recognizability-for-dpi-and-crawler)
|
||||||
|
|
||||||
⚓ Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared to other implementations of connecting to the Middle-End Proxy: non dramatically, but usual
|
⚓ Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared to other implementations of connecting to the Middle-End Proxy: non dramatically, but usual
|
||||||
@@ -103,8 +54,12 @@ We welcome ideas, architectural feedback, and pull requests.
|
|||||||
- [FAQ EN](docs/FAQ.en.md)
|
- [FAQ EN](docs/FAQ.en.md)
|
||||||
|
|
||||||
### Recognizability for DPI and crawler
|
### Recognizability for DPI and crawler
|
||||||
Since version 1.1.0.0, we have debugged masking perfectly: for all clients without "presenting" a key,
|
|
||||||
we transparently direct traffic to the target host!
|
On April 1, 2026, we became aware of a method for detecting MTProxy Fake-TLS,
|
||||||
|
based on the ECH extension and the ordering of cipher suites,
|
||||||
|
as well as an overall unique JA3/JA4 fingerprint
|
||||||
|
that does not occur in modern browsers:
|
||||||
|
we have already submitted initial changes to the Telegram Desktop developers and are working on updates for other clients.
|
||||||
|
|
||||||
- We consider this a breakthrough aspect, which has no stable analogues today
|
- We consider this a breakthrough aspect, which has no stable analogues today
|
||||||
- Based on this: if `telemt` configured correctly, **TLS mode is completely identical to real-life handshake + communication** with a specified host
|
- Based on this: if `telemt` configured correctly, **TLS mode is completely identical to real-life handshake + communication** with a specified host
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Cryptobench
|
// Cryptobench
|
||||||
use criterion::{black_box, criterion_group, Criterion};
|
use criterion::{Criterion, black_box, criterion_group};
|
||||||
|
|
||||||
fn bench_aes_ctr(c: &mut Criterion) {
|
fn bench_aes_ctr(c: &mut Criterion) {
|
||||||
c.bench_function("aes_ctr_encrypt_64kb", |b| {
|
c.bench_function("aes_ctr_encrypt_64kb", |b| {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "443:443"
|
- "443:443"
|
||||||
- "127.0.0.1:9090:9090"
|
- "127.0.0.1:9090:9090"
|
||||||
|
- "127.0.0.1:9091:9091"
|
||||||
# Allow caching 'proxy-secret' in read-only container
|
# Allow caching 'proxy-secret' in read-only container
|
||||||
working_dir: /run/telemt
|
working_dir: /run/telemt
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+6
-4
@@ -497,13 +497,14 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `direct_total` | `usize` | Direct-route upstream entries. |
|
| `direct_total` | `usize` | Direct-route upstream entries. |
|
||||||
| `socks4_total` | `usize` | SOCKS4 upstream entries. |
|
| `socks4_total` | `usize` | SOCKS4 upstream entries. |
|
||||||
| `socks5_total` | `usize` | SOCKS5 upstream entries. |
|
| `socks5_total` | `usize` | SOCKS5 upstream entries. |
|
||||||
|
| `shadowsocks_total` | `usize` | Shadowsocks upstream entries. |
|
||||||
|
|
||||||
#### `RuntimeUpstreamQualityUpstreamData`
|
#### `RuntimeUpstreamQualityUpstreamData`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `upstream_id` | `usize` | Runtime upstream index. |
|
| `upstream_id` | `usize` | Runtime upstream index. |
|
||||||
| `route_kind` | `string` | `direct`, `socks4`, `socks5`. |
|
| `route_kind` | `string` | `direct`, `socks4`, `socks5`, `shadowsocks`. |
|
||||||
| `address` | `string` | Upstream address (`direct` literal for direct route kind). |
|
| `address` | `string` | Upstream address (`direct` literal for direct route kind, `host:port` only for proxied upstreams). |
|
||||||
| `weight` | `u16` | Selection weight. |
|
| `weight` | `u16` | Selection weight. |
|
||||||
| `scopes` | `string` | Configured scope selector. |
|
| `scopes` | `string` | Configured scope selector. |
|
||||||
| `healthy` | `bool` | Current health flag. |
|
| `healthy` | `bool` | Current health flag. |
|
||||||
@@ -757,13 +758,14 @@ Note: the request contract is defined, but the corresponding route currently ret
|
|||||||
| `direct_total` | `usize` | Number of direct upstream entries. |
|
| `direct_total` | `usize` | Number of direct upstream entries. |
|
||||||
| `socks4_total` | `usize` | Number of SOCKS4 upstream entries. |
|
| `socks4_total` | `usize` | Number of SOCKS4 upstream entries. |
|
||||||
| `socks5_total` | `usize` | Number of SOCKS5 upstream entries. |
|
| `socks5_total` | `usize` | Number of SOCKS5 upstream entries. |
|
||||||
|
| `shadowsocks_total` | `usize` | Number of Shadowsocks upstream entries. |
|
||||||
|
|
||||||
#### `UpstreamStatus`
|
#### `UpstreamStatus`
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `upstream_id` | `usize` | Runtime upstream index. |
|
| `upstream_id` | `usize` | Runtime upstream index. |
|
||||||
| `route_kind` | `string` | Upstream route kind: `direct`, `socks4`, `socks5`. |
|
| `route_kind` | `string` | Upstream route kind: `direct`, `socks4`, `socks5`, `shadowsocks`. |
|
||||||
| `address` | `string` | Upstream address (`direct` for direct route kind). Authentication fields are intentionally omitted. |
|
| `address` | `string` | Upstream address (`direct` for direct route kind, `host:port` for Shadowsocks). Authentication fields are intentionally omitted. |
|
||||||
| `weight` | `u16` | Selection weight. |
|
| `weight` | `u16` | Selection weight. |
|
||||||
| `scopes` | `string` | Configured scope selector string. |
|
| `scopes` | `string` | Configured scope selector string. |
|
||||||
| `healthy` | `bool` | Current health flag. |
|
| `healthy` | `bool` | Current health flag. |
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+69
-40
@@ -1,107 +1,122 @@
|
|||||||
## How to set up "proxy sponsor" channel and statistics via @MTProxybot bot
|
## How to set up a "proxy sponsor" channel and statistics via the @MTProxybot
|
||||||
|
|
||||||
1. Go to @MTProxybot bot.
|
1. Go to the @MTProxybot.
|
||||||
2. Enter the command `/newproxy`
|
2. Enter the `/newproxy` command.
|
||||||
3. Send the server IP and port. For example: 1.2.3.4:443
|
3. Send your server's IP address and port. For example: `1.2.3.4:443`.
|
||||||
4. Open the config `nano /etc/telemt.toml`.
|
4. Open the configuration file: `nano /etc/telemt/telemt.toml`.
|
||||||
5. Copy and send the user secret from the [access.users] section to the bot.
|
5. Copy and send the user secret from the `[access.users]` section to the bot.
|
||||||
6. Copy the tag received from the bot. For example 1234567890abcdef1234567890abcdef.
|
6. Copy the tag provided by the bot. For example: `1234567890abcdef1234567890abcdef`.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> The link provided by the bot will not work. Do not copy or use it!
|
> The link provided by the bot will not work. Do not copy or use it!
|
||||||
7. Uncomment the ad_tag parameter and enter the tag received from the bot.
|
7. Uncomment the `ad_tag` parameter and enter the tag received from the bot.
|
||||||
8. Uncomment/add the parameter `use_middle_proxy = true`.
|
8. Uncomment or add the `use_middle_proxy = true` parameter.
|
||||||
|
|
||||||
Config example:
|
Configuration example:
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
ad_tag = "1234567890abcdef1234567890abcdef"
|
ad_tag = "1234567890abcdef1234567890abcdef"
|
||||||
use_middle_proxy = true
|
use_middle_proxy = true
|
||||||
```
|
```
|
||||||
9. Save the config. Ctrl+S -> Ctrl+X.
|
9. Save the changes (in nano: Ctrl+S -> Ctrl+X).
|
||||||
10. Restart telemt `systemctl restart telemt`.
|
10. Restart the telemt service: `systemctl restart telemt`.
|
||||||
11. In the bot, send the command /myproxies and select the added server.
|
11. Send the `/myproxies` command to the bot and select the added server.
|
||||||
12. Click the "Set promotion" button.
|
12. Click the "Set promotion" button.
|
||||||
13. Send a **public link** to the channel. Private channels cannot be added!
|
13. Send a **public link** to the channel. Private channels cannot be added!
|
||||||
14. Wait approximately 1 hour for the information to update on Telegram servers.
|
14. Wait for about 1 hour for the information to update on Telegram servers.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> You will not see the "proxy sponsor" if you are already subscribed to the channel.
|
> The sponsored channel will not be displayed to you if you are already subscribed to it.
|
||||||
|
|
||||||
**You can also set up different channels for different users.**
|
**You can also configure different sponsored channels for different users:**
|
||||||
```toml
|
```toml
|
||||||
[access.user_ad_tags]
|
[access.user_ad_tags]
|
||||||
hello = "ad_tag"
|
hello = "ad_tag"
|
||||||
hello2 = "ad_tag2"
|
hello2 = "ad_tag2"
|
||||||
```
|
```
|
||||||
|
|
||||||
## How many people can use 1 link
|
## Why do you need a middle proxy (ME)
|
||||||
|
https://github.com/telemt/telemt/discussions/167
|
||||||
|
|
||||||
By default, 1 link can be used by any number of people.
|
|
||||||
You can limit the number of IPs using the proxy.
|
## How many people can use one link
|
||||||
|
|
||||||
|
By default, an unlimited number of people can use a single link.
|
||||||
|
However, you can limit the number of unique IP addresses for each user:
|
||||||
```toml
|
```toml
|
||||||
[access.user_max_unique_ips]
|
[access.user_max_unique_ips]
|
||||||
hello = 1
|
hello = 1
|
||||||
```
|
```
|
||||||
This parameter limits how many unique IPs can use 1 link simultaneously. If one user disconnects, a second user can connect. Also, multiple users can sit behind the same IP.
|
This parameter sets the maximum number of unique IP addresses from which a single link can be used simultaneously. If the first user disconnects, a second one can connect. At the same time, multiple users can connect from a single IP address simultaneously (for example, devices on the same Wi-Fi network).
|
||||||
|
|
||||||
## How to create multiple different links
|
## How to create multiple different links
|
||||||
|
|
||||||
1. Generate the required number of secrets `openssl rand -hex 16`
|
1. Generate the required number of secrets using the command: `openssl rand -hex 16`.
|
||||||
2. Open the config `nano /etc/telemt.toml`
|
2. Open the configuration file: `nano /etc/telemt/telemt.toml`.
|
||||||
3. Add new users.
|
3. Add new users to the `[access.users]` section:
|
||||||
```toml
|
```toml
|
||||||
[access.users]
|
[access.users]
|
||||||
user1 = "00000000000000000000000000000001"
|
user1 = "00000000000000000000000000000001"
|
||||||
user2 = "00000000000000000000000000000002"
|
user2 = "00000000000000000000000000000002"
|
||||||
user3 = "00000000000000000000000000000003"
|
user3 = "00000000000000000000000000000003"
|
||||||
```
|
```
|
||||||
4. Save the config. Ctrl+S -> Ctrl+X. You don't need to restart telemt.
|
4. Save the configuration (Ctrl+S -> Ctrl+X). There is no need to restart the telemt service.
|
||||||
5. Get the links via
|
5. Get the ready-to-use links using the command:
|
||||||
```bash
|
```bash
|
||||||
curl -s http://127.0.0.1:9091/v1/users | jq
|
curl -s http://127.0.0.1:9091/v1/users | jq
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## "Unknown TLS SNI" error
|
||||||
|
Usually, this error occurs if you have changed the `tls_domain` parameter, but users continue to connect using old links with the previous domain.
|
||||||
|
|
||||||
|
If you need to allow connections with any domains (ignoring SNI mismatches), add the following parameters:
|
||||||
|
```toml
|
||||||
|
[censorship]
|
||||||
|
unknown_sni_action = "mask"
|
||||||
|
```
|
||||||
|
|
||||||
## How to view metrics
|
## How to view metrics
|
||||||
|
|
||||||
1. Open the config `nano /etc/telemt.toml`
|
1. Open the configuration file: `nano /etc/telemt/telemt.toml`.
|
||||||
2. Add the following parameters
|
2. Add the following parameters:
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
metrics_port = 9090
|
metrics_port = 9090
|
||||||
metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
||||||
```
|
```
|
||||||
3. Save the config. Ctrl+S -> Ctrl+X.
|
3. Save the changes (Ctrl+S -> Ctrl+X).
|
||||||
4. Metrics are available at SERVER_IP:9090/metrics.
|
4. After that, metrics will be available at: `SERVER_IP:9090/metrics`.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> "0.0.0.0/0" in metrics_whitelist opens access from any IP. Replace with your own IP. For example "1.2.3.4"
|
> The value `"0.0.0.0/0"` in `metrics_whitelist` opens access to metrics from any IP address. It is recommended to replace it with your personal IP, for example: `"1.2.3.4/32"`.
|
||||||
|
|
||||||
## Additional parameters
|
## Additional parameters
|
||||||
|
|
||||||
### Domain in link instead of IP
|
### Domain in the link instead of IP
|
||||||
To specify a domain in the links, add to the `[general.links]` section of the config file.
|
To display a domain instead of an IP address in the connection links, add the following lines to the configuration file:
|
||||||
```toml
|
```toml
|
||||||
[general.links]
|
[general.links]
|
||||||
public_host = "proxy.example.com"
|
public_host = "proxy.example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server connection limit
|
### Total server connection limit
|
||||||
Limits the total number of open connections to the server:
|
This parameter limits the total number of active connections to the server:
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
max_connections = 10000 # 0 - unlimited, 10000 - default
|
max_connections = 10000 # 0 - unlimited, 10000 - default
|
||||||
```
|
```
|
||||||
|
|
||||||
### Upstream Manager
|
### Upstream Manager
|
||||||
To specify an upstream, add to the `[[upstreams]]` section of the config.toml file:
|
To configure outbound connections (upstreams), add the corresponding parameters to the `[[upstreams]]` section of the configuration file:
|
||||||
#### Binding to IP
|
|
||||||
|
#### Binding to an outbound IP address
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
type = "direct"
|
type = "direct"
|
||||||
weight = 1
|
weight = 1
|
||||||
enabled = true
|
enabled = true
|
||||||
interface = "192.168.1.100" # Change to your outgoing IP
|
interface = "192.168.1.100" # Replace with your outbound IP
|
||||||
```
|
```
|
||||||
#### SOCKS4/5 as Upstream
|
|
||||||
- Without authentication:
|
#### Using SOCKS4/5 as an Upstream
|
||||||
|
- Without authorization:
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
type = "socks5" # Specify SOCKS4 or SOCKS5
|
type = "socks5" # Specify SOCKS4 or SOCKS5
|
||||||
@@ -110,7 +125,7 @@ weight = 1 # Set Weight for Scenarios
|
|||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
- With authentication:
|
- With authorization:
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
type = "socks5" # Specify SOCKS4 or SOCKS5
|
type = "socks5" # Specify SOCKS4 or SOCKS5
|
||||||
@@ -120,3 +135,17 @@ password = "pass" # Password for Auth on SOCKS-server
|
|||||||
weight = 1 # Set Weight for Scenarios
|
weight = 1 # Set Weight for Scenarios
|
||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Using Shadowsocks as an Upstream
|
||||||
|
For this method to work, the `use_middle_proxy = false` parameter must be set.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[general]
|
||||||
|
use_middle_proxy = false
|
||||||
|
|
||||||
|
[[upstreams]]
|
||||||
|
type = "shadowsocks"
|
||||||
|
url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@1.2.3.4:8388"
|
||||||
|
weight = 1
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
|||||||
+68
-40
@@ -1,106 +1,121 @@
|
|||||||
## Как настроить канал "спонсор прокси" и статистику через бота @MTProxybot
|
## Как настроить канал "спонсор прокси" и статистику через бота @MTProxybot
|
||||||
|
|
||||||
1. Зайти в бота @MTProxybot.
|
1. Зайдите в бота @MTProxybot.
|
||||||
2. Ввести команду `/newproxy`
|
2. Введите команду `/newproxy`.
|
||||||
3. Отправить IP и порт сервера. Например: 1.2.3.4:443
|
3. Отправьте IP-адрес и порт сервера. Например: `1.2.3.4:443`.
|
||||||
4. Открыть конфиг `nano /etc/telemt.toml`.
|
4. Откройте файл конфигурации: `nano /etc/telemt/telemt.toml`.
|
||||||
5. Скопировать и отправить боту секрет пользователя из раздела [access.users].
|
5. Скопируйте и отправьте боту секрет пользователя из раздела `[access.users]`.
|
||||||
6. Скопировать полученный tag у бота. Например 1234567890abcdef1234567890abcdef.
|
6. Скопируйте тег (tag), который выдаст бот. Например: `1234567890abcdef1234567890abcdef`.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Ссылка, которую выдает бот, не будет работать. Не копируйте и не используйте её!
|
> Ссылка, которую выдает бот, работать не будет. Не копируйте и не используйте её!
|
||||||
7. Раскомментировать параметр ad_tag и вписать tag, полученный у бота.
|
7. Раскомментируйте параметр `ad_tag` и впишите тег, полученный от бота.
|
||||||
8. Раскомментировать/добавить параметр use_middle_proxy = true.
|
8. Раскомментируйте или добавьте параметр `use_middle_proxy = true`.
|
||||||
|
|
||||||
Пример конфига:
|
Пример конфигурации:
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
ad_tag = "1234567890abcdef1234567890abcdef"
|
ad_tag = "1234567890abcdef1234567890abcdef"
|
||||||
use_middle_proxy = true
|
use_middle_proxy = true
|
||||||
```
|
```
|
||||||
9. Сохранить конфиг. Ctrl+S -> Ctrl+X.
|
9. Сохраните изменения (в nano: Ctrl+S -> Ctrl+X).
|
||||||
10. Перезапустить telemt `systemctl restart telemt`.
|
10. Перезапустите службу telemt: `systemctl restart telemt`.
|
||||||
11. В боте отправить команду /myproxies и выбрать добавленный сервер.
|
11. В боте отправьте команду `/myproxies` и выберите добавленный сервер.
|
||||||
12. Нажать кнопку "Set promotion".
|
12. Нажмите кнопку «Set promotion».
|
||||||
13. Отправить **публичную ссылку** на канал. Приватный канал добавить нельзя!
|
13. Отправьте **публичную ссылку** на канал. Приватные каналы добавлять нельзя!
|
||||||
14. Подождать примерно 1 час, пока информация обновится на серверах Telegram.
|
14. Подождите примерно 1 час, пока информация обновится на серверах Telegram.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> У вас не будет отображаться "спонсор прокси" если вы уже подписаны на канал.
|
> Спонсорский канал не будет у вас отображаться, если вы уже на него подписаны.
|
||||||
|
|
||||||
**Также вы можете настроить разные каналы для разных пользователей.**
|
**Вы также можете настроить разные спонсорские каналы для разных пользователей:**
|
||||||
```toml
|
```toml
|
||||||
[access.user_ad_tags]
|
[access.user_ad_tags]
|
||||||
hello = "ad_tag"
|
hello = "ad_tag"
|
||||||
hello2 = "ad_tag2"
|
hello2 = "ad_tag2"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Сколько человек может пользоваться 1 ссылкой
|
## Зачем нужен middle proxy (ME)
|
||||||
|
https://github.com/telemt/telemt/discussions/167
|
||||||
|
|
||||||
По умолчанию 1 ссылкой может пользоваться сколько угодно человек.
|
|
||||||
Вы можете ограничить число IP, использующих прокси.
|
## Сколько человек может пользоваться одной ссылкой
|
||||||
|
|
||||||
|
По умолчанию одной ссылкой может пользоваться неограниченное число людей.
|
||||||
|
Однако вы можете ограничить количество уникальных IP-адресов для каждого пользователя:
|
||||||
```toml
|
```toml
|
||||||
[access.user_max_unique_ips]
|
[access.user_max_unique_ips]
|
||||||
hello = 1
|
hello = 1
|
||||||
```
|
```
|
||||||
Этот параметр ограничивает, сколько уникальных IP может использовать 1 ссылку одновременно. Если один пользователь отключится, второй сможет подключиться. Также с одного IP может сидеть несколько пользователей.
|
Этот параметр задает максимальное количество уникальных IP-адресов, с которых можно одновременно использовать одну ссылку. Если первый пользователь отключится, второй сможет подключиться. При этом с одного IP-адреса могут подключаться несколько пользователей одновременно (например, устройства в одной Wi-Fi сети).
|
||||||
|
|
||||||
## Как сделать несколько разных ссылок
|
## Как создать несколько разных ссылок
|
||||||
|
|
||||||
1. Сгенерируйте нужное число секретов `openssl rand -hex 16`
|
1. Сгенерируйте необходимое количество секретов с помощью команды: `openssl rand -hex 16`.
|
||||||
2. Открыть конфиг `nano /etc/telemt.toml`
|
2. Откройте файл конфигурации: `nano /etc/telemt/telemt.toml`.
|
||||||
3. Добавить новых пользователей.
|
3. Добавьте новых пользователей в секцию `[access.users]`:
|
||||||
```toml
|
```toml
|
||||||
[access.users]
|
[access.users]
|
||||||
user1 = "00000000000000000000000000000001"
|
user1 = "00000000000000000000000000000001"
|
||||||
user2 = "00000000000000000000000000000002"
|
user2 = "00000000000000000000000000000002"
|
||||||
user3 = "00000000000000000000000000000003"
|
user3 = "00000000000000000000000000000003"
|
||||||
```
|
```
|
||||||
4. Сохранить конфиг. Ctrl+S -> Ctrl+X. Перезапускать telemt не нужно.
|
4. Сохраните конфигурацию (Ctrl+S -> Ctrl+X). Перезапускать службу telemt не нужно.
|
||||||
5. Получить ссылки через
|
5. Получите готовые ссылки с помощью команды:
|
||||||
```bash
|
```bash
|
||||||
curl -s http://127.0.0.1:9091/v1/users | jq
|
curl -s http://127.0.0.1:9091/v1/users | jq
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Ошибка "Unknown TLS SNI"
|
||||||
|
Обычно эта ошибка возникает, если вы изменили параметр `tls_domain`, но пользователи продолжают подключаться по старым ссылкам с прежним доменом.
|
||||||
|
|
||||||
|
Если необходимо разрешить подключение с любыми доменами (игнорируя несовпадения SNI), добавьте следующие параметры:
|
||||||
|
```toml
|
||||||
|
[censorship]
|
||||||
|
unknown_sni_action = "mask"
|
||||||
|
```
|
||||||
|
|
||||||
## Как посмотреть метрики
|
## Как посмотреть метрики
|
||||||
|
|
||||||
1. Открыть конфиг `nano /etc/telemt.toml`
|
1. Откройте файл конфигурации: `nano /etc/telemt/telemt.toml`.
|
||||||
2. Добавить следующие параметры
|
2. Добавьте следующие параметры:
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
metrics_port = 9090
|
metrics_port = 9090
|
||||||
metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
||||||
```
|
```
|
||||||
3. Сохранить конфиг. Ctrl+S -> Ctrl+X.
|
3. Сохраните изменения (Ctrl+S -> Ctrl+X).
|
||||||
4. Метрики доступны по адресу SERVER_IP:9090/metrics.
|
4. После этого метрики будут доступны по адресу: `SERVER_IP:9090/metrics`.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> "0.0.0.0/0" в metrics_whitelist открывает доступ с любого IP. Замените на свой ip. Например "1.2.3.4"
|
> Значение `"0.0.0.0/0"` в `metrics_whitelist` открывает доступ к метрикам с любого IP-адреса. Рекомендуется заменить его на ваш личный IP, например: `"1.2.3.4/32"`.
|
||||||
|
|
||||||
## Дополнительные параметры
|
## Дополнительные параметры
|
||||||
|
|
||||||
### Домен в ссылке вместо IP
|
### Домен в ссылке вместо IP
|
||||||
Чтобы указать домен в ссылках, добавьте в секцию `[general.links]` файла config.
|
Чтобы в ссылках для подключения отображался домен вместо IP-адреса, добавьте следующие строки в файл конфигурации:
|
||||||
```toml
|
```toml
|
||||||
[general.links]
|
[general.links]
|
||||||
public_host = "proxy.example.com"
|
public_host = "proxy.example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Общий лимит подключений к серверу
|
### Общий лимит подключений к серверу
|
||||||
Ограничивает общее число открытых подключений к серверу:
|
Этот параметр ограничивает общее количество активных подключений к серверу:
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
max_connections = 10000 # 0 - unlimited, 10000 - default
|
max_connections = 10000 # 0 - без ограничений, 10000 - по умолчанию
|
||||||
```
|
```
|
||||||
|
|
||||||
### Upstream Manager
|
### Upstream Manager
|
||||||
Чтобы указать апстрим, добавьте в секцию `[[upstreams]]` файла config.toml:
|
Для настройки исходящих подключений (апстримов) добавьте соответствующие параметры в секцию `[[upstreams]]` файла конфигурации:
|
||||||
#### Привязка к IP
|
|
||||||
|
#### Привязка к исходящему IP-адресу
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
type = "direct"
|
type = "direct"
|
||||||
weight = 1
|
weight = 1
|
||||||
enabled = true
|
enabled = true
|
||||||
interface = "192.168.1.100" # Change to your outgoing IP
|
interface = "192.168.1.100" # Замените на ваш исходящий IP
|
||||||
```
|
```
|
||||||
#### SOCKS4/5 как Upstream
|
|
||||||
|
#### Использование SOCKS4/5 в качестве Upstream
|
||||||
- Без авторизации:
|
- Без авторизации:
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
@@ -121,3 +136,16 @@ weight = 1 # Set Weight for Scenarios
|
|||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Использование Shadowsocks в качестве Upstream
|
||||||
|
Для работы этого метода требуется установить параметр `use_middle_proxy = false`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[general]
|
||||||
|
use_middle_proxy = false
|
||||||
|
|
||||||
|
[[upstreams]]
|
||||||
|
type = "shadowsocks"
|
||||||
|
url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@1.2.3.4:8388"
|
||||||
|
weight = 1
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ chmod +x /bin/telemt
|
|||||||
|
|
||||||
**0. Check port and generate secrets**
|
**0. Check port and generate secrets**
|
||||||
|
|
||||||
The port you have selected for use should be MISSING from the list, when:
|
The port you have selected for use should not be in the list:
|
||||||
```bash
|
```bash
|
||||||
netstat -lnp
|
netstat -lnp
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate 16 bytes/32 characters HEX with OpenSSL or another way:
|
Generate 16 bytes/32 characters in HEX format with OpenSSL or another way:
|
||||||
```bash
|
```bash
|
||||||
openssl rand -hex 16
|
openssl rand -hex 16
|
||||||
```
|
```
|
||||||
@@ -50,7 +50,7 @@ Save the obtained result somewhere. You will need it later!
|
|||||||
|
|
||||||
**1. Place your config to /etc/telemt/telemt.toml**
|
**1. Place your config to /etc/telemt/telemt.toml**
|
||||||
|
|
||||||
Create config directory:
|
Create the config directory:
|
||||||
```bash
|
```bash
|
||||||
mkdir /etc/telemt
|
mkdir /etc/telemt
|
||||||
```
|
```
|
||||||
@@ -59,7 +59,7 @@ Open nano
|
|||||||
```bash
|
```bash
|
||||||
nano /etc/telemt/telemt.toml
|
nano /etc/telemt/telemt.toml
|
||||||
```
|
```
|
||||||
paste your config
|
Insert your configuration:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# === General Settings ===
|
# === General Settings ===
|
||||||
@@ -94,7 +94,8 @@ then Ctrl+S -> Ctrl+X to save
|
|||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Replace the value of the hello parameter with the value you obtained in step 0.
|
> Replace the value of the hello parameter with the value you obtained in step 0.
|
||||||
> Replace the value of the tls_domain parameter with another website.
|
> Additionally, change the value of the tls_domain parameter to a different website.
|
||||||
|
> Changing the tls_domain parameter will break all links that use the old domain!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -105,14 +106,14 @@ useradd -d /opt/telemt -m -r -U telemt
|
|||||||
chown -R telemt:telemt /etc/telemt
|
chown -R telemt:telemt /etc/telemt
|
||||||
```
|
```
|
||||||
|
|
||||||
**3. Create service on /etc/systemd/system/telemt.service**
|
**3. Create service in /etc/systemd/system/telemt.service**
|
||||||
|
|
||||||
Open nano
|
Open nano
|
||||||
```bash
|
```bash
|
||||||
nano /etc/systemd/system/telemt.service
|
nano /etc/systemd/system/telemt.service
|
||||||
```
|
```
|
||||||
|
|
||||||
paste this Systemd Module
|
Insert this Systemd module:
|
||||||
```bash
|
```bash
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Telemt
|
Description=Telemt
|
||||||
@@ -147,13 +148,16 @@ systemctl daemon-reload
|
|||||||
|
|
||||||
**6.** For automatic startup at system boot, enter `systemctl enable telemt`
|
**6.** For automatic startup at system boot, enter `systemctl enable telemt`
|
||||||
|
|
||||||
**7.** To get the link(s), enter
|
**7.** To get the link(s), enter:
|
||||||
```bash
|
```bash
|
||||||
curl -s http://127.0.0.1:9091/v1/users | jq
|
curl -s http://127.0.0.1:9091/v1/users | jq
|
||||||
```
|
```
|
||||||
|
|
||||||
> Any number of people can use one link.
|
> Any number of people can use one link.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Only the command from step 7 can provide a working link. Do not try to create it yourself or copy it from anywhere if you are not sure what you are doing!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Telemt via Docker Compose
|
# Telemt via Docker Compose
|
||||||
@@ -181,6 +185,8 @@ docker compose down
|
|||||||
docker build -t telemt:local .
|
docker build -t telemt:local .
|
||||||
docker run --name telemt --restart unless-stopped \
|
docker run --name telemt --restart unless-stopped \
|
||||||
-p 443:443 \
|
-p 443:443 \
|
||||||
|
-p 9090:9090 \
|
||||||
|
-p 9091:9091 \
|
||||||
-e RUST_LOG=info \
|
-e RUST_LOG=info \
|
||||||
-v "$PWD/config.toml:/app/config.toml:ro" \
|
-v "$PWD/config.toml:/app/config.toml:ro" \
|
||||||
--read-only \
|
--read-only \
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ hello = "00000000000000000000000000000000"
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Замените значение параметра hello на значение, которое вы получили в пункте 0.
|
> Замените значение параметра hello на значение, которое вы получили в пункте 0.
|
||||||
> Так же замените значение параметра tls_domain на другой сайт.
|
> Так же замените значение параметра tls_domain на другой сайт.
|
||||||
|
> Изменение параметра tls_domain сделает нерабочими все ссылки, использующие старый домен!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -178,11 +179,13 @@ docker compose down
|
|||||||
> - По умолчанию публикуются порты 443:443, а контейнер запускается со сброшенными привилегиями (добавлена только `NET_BIND_SERVICE`)
|
> - По умолчанию публикуются порты 443:443, а контейнер запускается со сброшенными привилегиями (добавлена только `NET_BIND_SERVICE`)
|
||||||
> - Если вам действительно нужна сеть хоста (обычно это требуется только для некоторых конфигураций IPv6), раскомментируйте `network_mode: host`
|
> - Если вам действительно нужна сеть хоста (обычно это требуется только для некоторых конфигураций IPv6), раскомментируйте `network_mode: host`
|
||||||
|
|
||||||
**Запуск в Docker Compose**
|
**Запуск без Docker Compose**
|
||||||
```bash
|
```bash
|
||||||
docker build -t telemt:local .
|
docker build -t telemt:local .
|
||||||
docker run --name telemt --restart unless-stopped \
|
docker run --name telemt --restart unless-stopped \
|
||||||
-p 443:443 \
|
-p 443:443 \
|
||||||
|
-p 9090:9090 \
|
||||||
|
-p 9091:9091 \
|
||||||
-e RUST_LOG=info \
|
-e RUST_LOG=info \
|
||||||
-v "$PWD/config.toml:/app/config.toml:ro" \
|
-v "$PWD/config.toml:/app/config.toml:ro" \
|
||||||
--read-only \
|
--read-only \
|
||||||
|
|||||||
+18
-2
@@ -82,7 +82,7 @@ Die unten angegebenen `Default`-Werte sind Code-Defaults (bei fehlendem Schlüss
|
|||||||
|
|
||||||
| Feld | Gilt für | Typ | Pflicht | Default | Bedeutung |
|
| Feld | Gilt für | Typ | Pflicht | Default | Bedeutung |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `[[upstreams]].type` | alle Upstreams | `"direct" \| "socks4" \| "socks5"` | ja | n/a | Upstream-Transporttyp. |
|
| `[[upstreams]].type` | alle Upstreams | `"direct" \| "socks4" \| "socks5" \| "shadowsocks"` | ja | n/a | Upstream-Transporttyp. |
|
||||||
| `[[upstreams]].weight` | alle Upstreams | `u16` | nein | `1` | Basisgewicht für weighted-random Auswahl. |
|
| `[[upstreams]].weight` | alle Upstreams | `u16` | nein | `1` | Basisgewicht für weighted-random Auswahl. |
|
||||||
| `[[upstreams]].enabled` | alle Upstreams | `bool` | nein | `true` | Deaktivierte Einträge werden beim Start ignoriert. |
|
| `[[upstreams]].enabled` | alle Upstreams | `bool` | nein | `true` | Deaktivierte Einträge werden beim Start ignoriert. |
|
||||||
| `[[upstreams]].scopes` | alle Upstreams | `String` | nein | `""` | Komma-separierte Scope-Tags für Request-Routing. |
|
| `[[upstreams]].scopes` | alle Upstreams | `String` | nein | `""` | Komma-separierte Scope-Tags für Request-Routing. |
|
||||||
@@ -95,6 +95,8 @@ Die unten angegebenen `Default`-Werte sind Code-Defaults (bei fehlendem Schlüss
|
|||||||
| `interface` | `socks5` | `Option<String>` | nein | `null` | Wird nur genutzt, wenn `address` als `ip:port` angegeben ist. |
|
| `interface` | `socks5` | `Option<String>` | nein | `null` | Wird nur genutzt, wenn `address` als `ip:port` angegeben ist. |
|
||||||
| `username` | `socks5` | `Option<String>` | nein | `null` | SOCKS5 Benutzername. |
|
| `username` | `socks5` | `Option<String>` | nein | `null` | SOCKS5 Benutzername. |
|
||||||
| `password` | `socks5` | `Option<String>` | nein | `null` | SOCKS5 Passwort. |
|
| `password` | `socks5` | `Option<String>` | nein | `null` | SOCKS5 Passwort. |
|
||||||
|
| `url` | `shadowsocks` | `String` | ja | n/a | Shadowsocks-SIP002-URL (`ss://...`). In Runtime-APIs wird nur `host:port` offengelegt. |
|
||||||
|
| `interface` | `shadowsocks` | `Option<String>` | nein | `null` | Optionales ausgehendes Bind-Interface oder lokale Literal-IP. |
|
||||||
|
|
||||||
### Runtime-Regeln (wichtig)
|
### Runtime-Regeln (wichtig)
|
||||||
|
|
||||||
@@ -115,6 +117,7 @@ Die unten angegebenen `Default`-Werte sind Code-Defaults (bei fehlendem Schlüss
|
|||||||
8. Im ME-Modus wird der gewählte Upstream auch für den ME-TCP-Dial-Pfad verwendet.
|
8. Im ME-Modus wird der gewählte Upstream auch für den ME-TCP-Dial-Pfad verwendet.
|
||||||
9. Im ME-Modus ist bei `direct` mit bind/interface die STUN-Reflection bind-aware für KDF-Adressmaterial.
|
9. Im ME-Modus ist bei `direct` mit bind/interface die STUN-Reflection bind-aware für KDF-Adressmaterial.
|
||||||
10. Im ME-Modus werden bei SOCKS-Upstream `BND.ADDR/BND.PORT` für KDF verwendet, wenn gültig/öffentlich und gleiche IP-Familie.
|
10. Im ME-Modus werden bei SOCKS-Upstream `BND.ADDR/BND.PORT` für KDF verwendet, wenn gültig/öffentlich und gleiche IP-Familie.
|
||||||
|
11. `shadowsocks`-Upstreams erfordern `general.use_middle_proxy = false`. Mit aktiviertem ME-Modus schlägt das Laden der Config sofort fehl.
|
||||||
|
|
||||||
## Upstream-Konfigurationsbeispiele
|
## Upstream-Konfigurationsbeispiele
|
||||||
|
|
||||||
@@ -150,7 +153,20 @@ weight = 2
|
|||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Beispiel 4: Gemischte Upstreams mit Scopes
|
### Beispiel 4: Shadowsocks-Upstream
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[general]
|
||||||
|
use_middle_proxy = false
|
||||||
|
|
||||||
|
[[upstreams]]
|
||||||
|
type = "shadowsocks"
|
||||||
|
url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@198.51.100.50:8388"
|
||||||
|
weight = 2
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel 5: Gemischte Upstreams mit Scopes
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
|
|||||||
+18
-2
@@ -82,7 +82,7 @@ Defaults below are code defaults (used when a key is omitted), not necessarily v
|
|||||||
|
|
||||||
| Field | Applies to | Type | Required | Default | Meaning |
|
| Field | Applies to | Type | Required | Default | Meaning |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `[[upstreams]].type` | all upstreams | `"direct" \| "socks4" \| "socks5"` | yes | n/a | Upstream transport type. |
|
| `[[upstreams]].type` | all upstreams | `"direct" \| "socks4" \| "socks5" \| "shadowsocks"` | yes | n/a | Upstream transport type. |
|
||||||
| `[[upstreams]].weight` | all upstreams | `u16` | no | `1` | Base weight for weighted-random selection. |
|
| `[[upstreams]].weight` | all upstreams | `u16` | no | `1` | Base weight for weighted-random selection. |
|
||||||
| `[[upstreams]].enabled` | all upstreams | `bool` | no | `true` | Disabled entries are ignored at startup. |
|
| `[[upstreams]].enabled` | all upstreams | `bool` | no | `true` | Disabled entries are ignored at startup. |
|
||||||
| `[[upstreams]].scopes` | all upstreams | `String` | no | `""` | Comma-separated scope tags for request-level routing. |
|
| `[[upstreams]].scopes` | all upstreams | `String` | no | `""` | Comma-separated scope tags for request-level routing. |
|
||||||
@@ -95,6 +95,8 @@ Defaults below are code defaults (used when a key is omitted), not necessarily v
|
|||||||
| `interface` | `socks5` | `Option<String>` | no | `null` | Used only for SOCKS server `ip:port` dial path. |
|
| `interface` | `socks5` | `Option<String>` | no | `null` | Used only for SOCKS server `ip:port` dial path. |
|
||||||
| `username` | `socks5` | `Option<String>` | no | `null` | SOCKS5 username auth. |
|
| `username` | `socks5` | `Option<String>` | no | `null` | SOCKS5 username auth. |
|
||||||
| `password` | `socks5` | `Option<String>` | no | `null` | SOCKS5 password auth. |
|
| `password` | `socks5` | `Option<String>` | no | `null` | SOCKS5 password auth. |
|
||||||
|
| `url` | `shadowsocks` | `String` | yes | n/a | Shadowsocks SIP002 URL (`ss://...`). Only `host:port` is exposed in runtime APIs. |
|
||||||
|
| `interface` | `shadowsocks` | `Option<String>` | no | `null` | Optional outgoing bind interface or literal local IP. |
|
||||||
|
|
||||||
### Runtime rules (important)
|
### Runtime rules (important)
|
||||||
|
|
||||||
@@ -115,6 +117,7 @@ Defaults below are code defaults (used when a key is omitted), not necessarily v
|
|||||||
8. In ME mode, the selected upstream is also used for ME TCP dial path.
|
8. In ME mode, the selected upstream is also used for ME TCP dial path.
|
||||||
9. In ME mode for `direct` upstream with bind/interface, STUN reflection logic is bind-aware for KDF source material.
|
9. In ME mode for `direct` upstream with bind/interface, STUN reflection logic is bind-aware for KDF source material.
|
||||||
10. In ME mode for SOCKS upstream, SOCKS `BND.ADDR/BND.PORT` is used for KDF when it is valid/public for the same family.
|
10. In ME mode for SOCKS upstream, SOCKS `BND.ADDR/BND.PORT` is used for KDF when it is valid/public for the same family.
|
||||||
|
11. `shadowsocks` upstreams require `general.use_middle_proxy = false`. Config load fails fast if ME mode is enabled.
|
||||||
|
|
||||||
## Upstream Configuration Examples
|
## Upstream Configuration Examples
|
||||||
|
|
||||||
@@ -150,7 +153,20 @@ weight = 2
|
|||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example 4: Mixed upstreams with scopes
|
### Example 4: Shadowsocks upstream
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[general]
|
||||||
|
use_middle_proxy = false
|
||||||
|
|
||||||
|
[[upstreams]]
|
||||||
|
type = "shadowsocks"
|
||||||
|
url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@198.51.100.50:8388"
|
||||||
|
weight = 2
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 5: Mixed upstreams with scopes
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
|
|||||||
+18
-2
@@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
| Поле | Применимость | Тип | Обязательно | Default | Назначение |
|
| Поле | Применимость | Тип | Обязательно | Default | Назначение |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `[[upstreams]].type` | все upstream | `"direct" \| "socks4" \| "socks5"` | да | n/a | Тип upstream транспорта. |
|
| `[[upstreams]].type` | все upstream | `"direct" \| "socks4" \| "socks5" \| "shadowsocks"` | да | n/a | Тип upstream транспорта. |
|
||||||
| `[[upstreams]].weight` | все upstream | `u16` | нет | `1` | Базовый вес в weighted-random выборе. |
|
| `[[upstreams]].weight` | все upstream | `u16` | нет | `1` | Базовый вес в weighted-random выборе. |
|
||||||
| `[[upstreams]].enabled` | все upstream | `bool` | нет | `true` | Выключенные записи игнорируются на старте. |
|
| `[[upstreams]].enabled` | все upstream | `bool` | нет | `true` | Выключенные записи игнорируются на старте. |
|
||||||
| `[[upstreams]].scopes` | все upstream | `String` | нет | `""` | Список scope-токенов через запятую для маршрутизации. |
|
| `[[upstreams]].scopes` | все upstream | `String` | нет | `""` | Список scope-токенов через запятую для маршрутизации. |
|
||||||
@@ -95,6 +95,8 @@
|
|||||||
| `interface` | `socks5` | `Option<String>` | нет | `null` | Используется только если `address` задан как `ip:port`. |
|
| `interface` | `socks5` | `Option<String>` | нет | `null` | Используется только если `address` задан как `ip:port`. |
|
||||||
| `username` | `socks5` | `Option<String>` | нет | `null` | Логин SOCKS5 auth. |
|
| `username` | `socks5` | `Option<String>` | нет | `null` | Логин SOCKS5 auth. |
|
||||||
| `password` | `socks5` | `Option<String>` | нет | `null` | Пароль SOCKS5 auth. |
|
| `password` | `socks5` | `Option<String>` | нет | `null` | Пароль SOCKS5 auth. |
|
||||||
|
| `url` | `shadowsocks` | `String` | да | n/a | Shadowsocks SIP002 URL (`ss://...`). В runtime API раскрывается только `host:port`. |
|
||||||
|
| `interface` | `shadowsocks` | `Option<String>` | нет | `null` | Необязательный исходящий bind-интерфейс или literal локальный IP. |
|
||||||
|
|
||||||
### Runtime-правила
|
### Runtime-правила
|
||||||
|
|
||||||
@@ -115,6 +117,7 @@
|
|||||||
8. В ME-режиме выбранный upstream также используется для ME TCP dial path.
|
8. В ME-режиме выбранный upstream также используется для ME TCP dial path.
|
||||||
9. В ME-режиме для `direct` upstream с bind/interface STUN-рефлексия выполняется bind-aware для KDF материала.
|
9. В ME-режиме для `direct` upstream с bind/interface STUN-рефлексия выполняется bind-aware для KDF материала.
|
||||||
10. В ME-режиме для SOCKS upstream используются `BND.ADDR/BND.PORT` для KDF, если адрес валиден/публичен и соответствует IP family.
|
10. В ME-режиме для SOCKS upstream используются `BND.ADDR/BND.PORT` для KDF, если адрес валиден/публичен и соответствует IP family.
|
||||||
|
11. `shadowsocks` upstream требует `general.use_middle_proxy = false`. При включенном ME-режиме конфиг отклоняется при загрузке.
|
||||||
|
|
||||||
## Примеры конфигурации Upstreams
|
## Примеры конфигурации Upstreams
|
||||||
|
|
||||||
@@ -150,7 +153,20 @@ weight = 2
|
|||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Пример 4: смешанные upstream с scopes
|
### Пример 4: Shadowsocks upstream
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[general]
|
||||||
|
use_middle_proxy = false
|
||||||
|
|
||||||
|
[[upstreams]]
|
||||||
|
type = "shadowsocks"
|
||||||
|
url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@198.51.100.50:8388"
|
||||||
|
weight = 2
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 5: смешанные upstream с scopes
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
<img src="https://gist.githubusercontent.com/avbor/1f8a128e628f47249aae6e058a57610b/raw/19013276c035e91058e0a9799ab145f8e70e3ff5/scheme.svg">
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
- **Server A** (__conditionally Russian Federation_):\
|
||||||
|
Entry point, receives Telegram proxy user traffic via **HAProxy** (port `443`)\
|
||||||
|
and sends it to the tunnel to Server **B**.\
|
||||||
|
Internal IP in the tunnel — `10.10.10.2`\
|
||||||
|
Port for HAProxy clients — `443\tcp`
|
||||||
|
- **Server B** (_conditionally Netherlands_):\
|
||||||
|
Exit point, runs **telemt** and accepts client connections through Server **A**.\
|
||||||
|
The server must have unrestricted access to Telegram servers.\
|
||||||
|
Internal IP in the tunnel — `10.10.10.1`\
|
||||||
|
AmneziaWG port — `8443\udp`\
|
||||||
|
Port for telemt clients — `443\tcp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1. Setting up the AmneziaWG tunnel (A <-> B)
|
||||||
|
[AmneziaWG](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module) must be installed on all servers.\
|
||||||
|
All following commands are given for **Ubuntu 24.04**.\
|
||||||
|
For RHEL-based distributions, installation instructions are available at the link above.
|
||||||
|
|
||||||
|
### Installing AmneziaWG (Servers A and B)
|
||||||
|
The following steps must be performed on each server:
|
||||||
|
|
||||||
|
#### 1. Adding the AmneziaWG repository and installing required packages:
|
||||||
|
```bash
|
||||||
|
sudo apt install -y software-properties-common python3-launchpadlib gnupg2 linux-headers-$(uname -r) && \
|
||||||
|
sudo add-apt-repository ppa:amnezia/ppa && \
|
||||||
|
sudo apt-get install -y amneziawg
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Generating a unique key pair:
|
||||||
|
```bash
|
||||||
|
cd /etc/amnezia/amneziawg && \
|
||||||
|
awg genkey | tee private.key | awg pubkey > public.key
|
||||||
|
```
|
||||||
|
|
||||||
|
As a result, you will get two files in the `/etc/amnezia/amneziawg` folder:\
|
||||||
|
`private.key` - private, and\
|
||||||
|
`public.key` - public server keys
|
||||||
|
|
||||||
|
#### 3. Configuring network interfaces:
|
||||||
|
Obfuscation parameters `S1`, `S2`, `H1`, `H2`, `H3`, `H4` must be strictly identical on both servers.\
|
||||||
|
Parameters `Jc`, `Jmin` and `Jmax` can differ.\
|
||||||
|
Parameters `I1-I5` ([Custom Protocol Signature](https://docs.amnezia.org/documentation/amnezia-wg/)) must be specified on the client side (Server **A**).
|
||||||
|
|
||||||
|
Recommendations for choosing values:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Jc — 1 ≤ Jc ≤ 128; from 4 to 12 inclusive
|
||||||
|
Jmin — Jmax > Jmin < 1280*; recommended 8
|
||||||
|
Jmax — Jmin < Jmax ≤ 1280*; recommended 80
|
||||||
|
S1 — S1 ≤ 1132* (1280* - 148 = 1132); S1 + 56 ≠ S2;
|
||||||
|
recommended range from 15 to 150 inclusive
|
||||||
|
S2 — S2 ≤ 1188* (1280* - 92 = 1188);
|
||||||
|
recommended range from 15 to 150 inclusive
|
||||||
|
H1/H2/H3/H4 — must be unique and differ from each other;
|
||||||
|
recommended range from 5 to 2147483647 inclusive
|
||||||
|
|
||||||
|
* It is assumed that the Internet connection has an MTU of 1280.
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> It is recommended to use your own, unique values.\
|
||||||
|
> You can use the [generator](https://htmlpreview.github.io/?https://gist.githubusercontent.com/avbor/955782b5c37b06240b243aa375baeac5/raw/13f5517ca473b47c412b9a99407066de973732bd/awg-gen.html) to select parameters.
|
||||||
|
|
||||||
|
#### Server B Configuration (Netherlands):
|
||||||
|
|
||||||
|
Create the interface configuration file (`awg0`)
|
||||||
|
```bash
|
||||||
|
nano /etc/amnezia/amneziawg/awg0.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
File content
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
Address = 10.10.10.1/24
|
||||||
|
ListenPort = 8443
|
||||||
|
PrivateKey = <PRIVATE_KEY_SERVER_B>
|
||||||
|
SaveConfig = true
|
||||||
|
Jc = 4
|
||||||
|
Jmin = 8
|
||||||
|
Jmax = 80
|
||||||
|
S1 = 29
|
||||||
|
S2 = 15
|
||||||
|
S3 = 18
|
||||||
|
S4 = 0
|
||||||
|
H1 = 2087563914
|
||||||
|
H2 = 188817757
|
||||||
|
H3 = 101784570
|
||||||
|
H4 = 432174303
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <PUBLIC_KEY_SERVER_A>
|
||||||
|
AllowedIPs = 10.10.10.2/32
|
||||||
|
```
|
||||||
|
`ListenPort` - the port on which the server will wait for connections, you can choose any free one.\
|
||||||
|
`<PRIVATE_KEY_SERVER_B>` - the content of the `private.key` file from Server **B**.\
|
||||||
|
`<PUBLIC_KEY_SERVER_A>` - the content of the `public.key` file from Server **A**.
|
||||||
|
|
||||||
|
Open the port on the firewall (if enabled):
|
||||||
|
```bash
|
||||||
|
sudo ufw allow from <PUBLIC_IP_SERVER_A> to any port 8443 proto udp
|
||||||
|
```
|
||||||
|
|
||||||
|
`<PUBLIC_IP_SERVER_A>` - the external IP address of Server **A**.
|
||||||
|
|
||||||
|
#### Server A Configuration (Russian Federation):
|
||||||
|
Create the interface configuration file (awg0)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /etc/amnezia/amneziawg/awg0.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
File content
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
Address = 10.10.10.2/24
|
||||||
|
PrivateKey = <PRIVATE_KEY_SERVER_A>
|
||||||
|
Jc = 4
|
||||||
|
Jmin = 8
|
||||||
|
Jmax = 80
|
||||||
|
S1 = 29
|
||||||
|
S2 = 15
|
||||||
|
S3 = 18
|
||||||
|
S4 = 0
|
||||||
|
H1 = 2087563914
|
||||||
|
H2 = 188817757
|
||||||
|
H3 = 101784570
|
||||||
|
H4 = 432174303
|
||||||
|
I1 = <b 0xc10000000108981eba846e21f74e00>
|
||||||
|
I2 = <b 0xc20000000108981eba846e21f74e00>
|
||||||
|
I3 = <b 0xc30000000108981eba846e21f74e00>
|
||||||
|
I4 = <b 0x43981eba846e21f74e>
|
||||||
|
I5 = <b 0x43981eba846e21f74e>
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <PUBLIC_KEY_SERVER_B>
|
||||||
|
Endpoint = <PUBLIC_IP_SERVER_B>:8443
|
||||||
|
AllowedIPs = 10.10.10.1/32
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
```
|
||||||
|
|
||||||
|
`<PRIVATE_KEY_SERVER_A>` - the content of the `private.key` file from Server **A**.\
|
||||||
|
`<PUBLIC_KEY_SERVER_B>` - the content of the `public.key` file from Server **B**.\
|
||||||
|
`<PUBLIC_IP_SERVER_B>` - the public IP address of Server **B**.
|
||||||
|
|
||||||
|
Enable the tunnel on both servers:
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable --now awg-quick@awg0
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure Server B is accessible from Server A through the tunnel.
|
||||||
|
```bash
|
||||||
|
ping 10.10.10.1
|
||||||
|
PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
|
||||||
|
64 bytes from 10.10.10.1: icmp_seq=1 ttl=64 time=35.1 ms
|
||||||
|
64 bytes from 10.10.10.1: icmp_seq=2 ttl=64 time=35.0 ms
|
||||||
|
64 bytes from 10.10.10.1: icmp_seq=3 ttl=64 time=35.1 ms
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2. Installing telemt on Server B (conditionally Netherlands)
|
||||||
|
Installation and configuration are described [here](https://github.com/telemt/telemt/blob/main/docs/QUICK_START_GUIDE.ru.md) or [here](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
|
||||||
|
It is assumed that telemt expects connections on port `443\tcp`.
|
||||||
|
|
||||||
|
In the telemt config, you must enable the `Proxy` protocol and restrict connections to it only through the tunnel.
|
||||||
|
```toml
|
||||||
|
[server]
|
||||||
|
port = 443
|
||||||
|
listen_addr_ipv4 = "10.10.10.1"
|
||||||
|
proxy_protocol = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, for correct link generation, specify the FQDN or IP address and port of Server `A`
|
||||||
|
```toml
|
||||||
|
[general.links]
|
||||||
|
show = "*"
|
||||||
|
public_host = "<FQDN_OR_IP_SERVER_A>"
|
||||||
|
public_port = 443
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the port on the firewall (if enabled):
|
||||||
|
```bash
|
||||||
|
sudo ufw allow from 10.10.10.2 to any port 443 proto tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3. Configuring HAProxy on Server A (Russian Federation)
|
||||||
|
Since the version in the standard Ubuntu repository is relatively old, it makes sense to use the official Docker image.\
|
||||||
|
[Instructions](https://docs.docker.com/engine/install/ubuntu/) for installing Docker on Ubuntu.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> By default, regular users do not have rights to use ports < 1024.
|
||||||
|
> Attempts to run HAProxy on port 443 can lead to errors:
|
||||||
|
> ```
|
||||||
|
> [ALERT] (8) : Binding [/usr/local/etc/haproxy/haproxy.cfg:17] for frontend tcp_in_443:
|
||||||
|
> protocol tcpv4: cannot bind socket (Permission denied) for [0.0.0.0:443].
|
||||||
|
> ```
|
||||||
|
> There are two simple ways to bypass this restriction, choose one:
|
||||||
|
> 1. At the OS level, change the net.ipv4.ip_unprivileged_port_start setting to allow users to use all ports:
|
||||||
|
> ```
|
||||||
|
> echo "net.ipv4.ip_unprivileged_port_start = 0" | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
|
||||||
|
> ```
|
||||||
|
> or
|
||||||
|
>
|
||||||
|
> 2. Run HAProxy as root:
|
||||||
|
> Uncomment the `user: "root"` parameter in docker-compose.yaml.
|
||||||
|
|
||||||
|
#### Create a folder for HAProxy:
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/docker-compose/haproxy && cd $_
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create the docker-compose.yaml file
|
||||||
|
`nano docker-compose.yaml`
|
||||||
|
|
||||||
|
File content
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
haproxy:
|
||||||
|
image: haproxy:latest
|
||||||
|
container_name: haproxy
|
||||||
|
restart: unless-stopped
|
||||||
|
# user: "root"
|
||||||
|
network_mode: "host"
|
||||||
|
volumes:
|
||||||
|
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
max-file: "1"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create the haproxy.cfg config file
|
||||||
|
Accept connections on port 443\tcp and send them through the tunnel to Server `B` 10.10.10.1:443
|
||||||
|
|
||||||
|
`nano haproxy.cfg`
|
||||||
|
|
||||||
|
File content
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
global
|
||||||
|
log stdout format raw local0
|
||||||
|
maxconn 10000
|
||||||
|
|
||||||
|
defaults
|
||||||
|
log global
|
||||||
|
mode tcp
|
||||||
|
option tcplog
|
||||||
|
option clitcpka
|
||||||
|
option srvtcpka
|
||||||
|
timeout connect 5s
|
||||||
|
timeout client 2h
|
||||||
|
timeout server 2h
|
||||||
|
timeout check 5s
|
||||||
|
|
||||||
|
frontend tcp_in_443
|
||||||
|
bind *:443
|
||||||
|
maxconn 8000
|
||||||
|
option tcp-smart-accept
|
||||||
|
default_backend telemt_nodes
|
||||||
|
|
||||||
|
backend telemt_nodes
|
||||||
|
option tcp-smart-connect
|
||||||
|
server server_a 10.10.10.1:443 check inter 5s rise 2 fall 3 send-proxy-v2
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
> [!WARNING]
|
||||||
|
> **The file must end with an empty line, otherwise HAProxy will not start!**
|
||||||
|
|
||||||
|
#### Allow port 443\tcp in the firewall (if enabled)
|
||||||
|
```bash
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Start the HAProxy container
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
If everything is configured correctly, you can now try connecting Telegram clients using links from the telemt log\api.
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
<img src="https://gist.githubusercontent.com/avbor/1f8a128e628f47249aae6e058a57610b/raw/19013276c035e91058e0a9799ab145f8e70e3ff5/scheme.svg">
|
||||||
|
|
||||||
|
## Концепция
|
||||||
|
- **Сервер A** (_РФ_):\
|
||||||
|
Точка входа, принимает трафик пользователей Telegram-прокси через **HAProxy** (порт `443`)\
|
||||||
|
и отправляет в туннель на Сервер **B**.\
|
||||||
|
Внутренний IP в туннеле — `10.10.10.2`\
|
||||||
|
Порт для клиентов HAProxy — `443\tcp`
|
||||||
|
- **Сервер B** (_условно Нидерланды_):\
|
||||||
|
Точка выхода, на нем работает **telemt** и принимает подключения клиентов через Сервер **A**.\
|
||||||
|
На сервере должен быть неограниченный доступ до серверов Telegram.\
|
||||||
|
Внутренний IP в туннеле — `10.10.10.1`\
|
||||||
|
Порт AmneziaWG — `8443\udp`\
|
||||||
|
Порт для клиентов telemt — `443\tcp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 1. Настройка туннеля AmneziaWG (A <-> B)
|
||||||
|
|
||||||
|
На всех серверах необходимо установить [amneziawg](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module).\
|
||||||
|
Далее все команды даны для **Ununtu 24.04**.\
|
||||||
|
Для RHEL-based дистрибутивов инструкция по установке есть по ссылке выше.
|
||||||
|
|
||||||
|
### Установка AmneziaWG (Сервера A и B)
|
||||||
|
На каждом из серверов необходимо выполнить следующие шаги:
|
||||||
|
|
||||||
|
#### 1. Добавление репозитория AmneziaWG и установка необходимых пакетов:
|
||||||
|
```bash
|
||||||
|
sudo apt install -y software-properties-common python3-launchpadlib gnupg2 linux-headers-$(uname -r) && \
|
||||||
|
sudo add-apt-repository ppa:amnezia/ppa && \
|
||||||
|
sudo apt-get install -y amneziawg
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Генерация уникальной пары ключей:
|
||||||
|
```bash
|
||||||
|
cd /etc/amnezia/amneziawg && \
|
||||||
|
awg genkey | tee private.key | awg pubkey > public.key
|
||||||
|
```
|
||||||
|
В результате вы получите в папке `/etc/amnezia/amneziawg` два файла:\
|
||||||
|
`private.key` - приватный и\
|
||||||
|
`public.key` - публичный ключи сервера
|
||||||
|
|
||||||
|
#### 3. Настройка сетевых интерфейсов:
|
||||||
|
|
||||||
|
Параметры обфускации `S1`, `S2`, `H1`, `H2`, `H3`, `H4` должны быть строго идентичными на обоих серверах.\
|
||||||
|
Параметры `Jc`, `Jmin` и `Jmax` могут отличатся.\
|
||||||
|
Параметры `I1-I5` ([Custom Protocol Signature](https://docs.amnezia.org/documentation/amnezia-wg/)) нужно указывать на стороне _клиента_ (Сервер **А**).
|
||||||
|
|
||||||
|
Рекомендации по выбору значений:
|
||||||
|
```text
|
||||||
|
Jc — 1 ≤ Jc ≤ 128; от 4 до 12 включительно
|
||||||
|
Jmin — Jmax > Jmin < 1280*; рекомендовано 8
|
||||||
|
Jmax — Jmin < Jmax ≤ 1280*; рекомендовано 80
|
||||||
|
S1 — S1 ≤ 1132* (1280* - 148 = 1132); S1 + 56 ≠ S2;
|
||||||
|
рекомендованный диапазон от 15 до 150 включительно
|
||||||
|
S2 — S2 ≤ 1188* (1280* - 92 = 1188);
|
||||||
|
рекомендованный диапазон от 15 до 150 включительно
|
||||||
|
H1/H2/H3/H4 — должны быть уникальны и отличаться друг от друга;
|
||||||
|
рекомендованный диапазон от 5 до 2147483647 включительно
|
||||||
|
|
||||||
|
* Предполагается, что подключение к Интернету имеет MTU 1280.
|
||||||
|
```
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Рекомендуется использовать собственные, уникальные значения.\
|
||||||
|
> Для выбора параметров можете воспользоваться [генератором](https://htmlpreview.github.io/?https://gist.githubusercontent.com/avbor/955782b5c37b06240b243aa375baeac5/raw/13f5517ca473b47c412b9a99407066de973732bd/awg-gen.html).
|
||||||
|
|
||||||
|
#### Конфигурация Сервера B (_Нидерланды_):
|
||||||
|
|
||||||
|
Создаем файл конфигурации интерфейса (`awg0`)
|
||||||
|
```bash
|
||||||
|
nano /etc/amnezia/amneziawg/awg0.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Содержимое файла
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
Address = 10.10.10.1/24
|
||||||
|
ListenPort = 8443
|
||||||
|
PrivateKey = <PRIVATE_KEY_SERVER_B>
|
||||||
|
SaveConfig = true
|
||||||
|
Jc = 4
|
||||||
|
Jmin = 8
|
||||||
|
Jmax = 80
|
||||||
|
S1 = 29
|
||||||
|
S2 = 15
|
||||||
|
S3 = 18
|
||||||
|
S4 = 0
|
||||||
|
H1 = 2087563914
|
||||||
|
H2 = 188817757
|
||||||
|
H3 = 101784570
|
||||||
|
H4 = 432174303
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <PUBLIC_KEY_SERVER_A>
|
||||||
|
AllowedIPs = 10.10.10.2/32
|
||||||
|
```
|
||||||
|
|
||||||
|
`ListenPort` - порт, на котором сервер будет ждать подключения, можете выбрать любой свободный.\
|
||||||
|
`<PRIVATE_KEY_SERVER_B>` - содержимое файла `private.key` с сервера **B**.\
|
||||||
|
`<PUBLIC_KEY_SERVER_A>` - содержимое файла `public.key` с сервера **A**.
|
||||||
|
|
||||||
|
Открываем порт на фаерволе (если включен):
|
||||||
|
```bash
|
||||||
|
sudo ufw allow from <PUBLIC_IP_SERVER_A> to any port 8443 proto udp
|
||||||
|
```
|
||||||
|
|
||||||
|
`<PUBLIC_IP_SERVER_A>` - внешний IP адрес Сервера **A**.
|
||||||
|
|
||||||
|
#### Конфигурация Сервера A (_РФ_):
|
||||||
|
|
||||||
|
Создаем файл конфигурации интерфейса (`awg0`)
|
||||||
|
```bash
|
||||||
|
nano /etc/amnezia/amneziawg/awg0.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Содержимое файла
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
Address = 10.10.10.2/24
|
||||||
|
PrivateKey = <PRIVATE_KEY_SERVER_A>
|
||||||
|
Jc = 4
|
||||||
|
Jmin = 8
|
||||||
|
Jmax = 80
|
||||||
|
S1 = 29
|
||||||
|
S2 = 15
|
||||||
|
S3 = 18
|
||||||
|
S4 = 0
|
||||||
|
H1 = 2087563914
|
||||||
|
H2 = 188817757
|
||||||
|
H3 = 101784570
|
||||||
|
H4 = 432174303
|
||||||
|
I1 = <b 0xc10000000108981eba846e21f74e00>
|
||||||
|
I2 = <b 0xc20000000108981eba846e21f74e00>
|
||||||
|
I3 = <b 0xc30000000108981eba846e21f74e00>
|
||||||
|
I4 = <b 0x43981eba846e21f74e>
|
||||||
|
I5 = <b 0x43981eba846e21f74e>
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <PUBLIC_KEY_SERVER_B>
|
||||||
|
Endpoint = <PUBLIC_IP_SERVER_B>:8443
|
||||||
|
AllowedIPs = 10.10.10.1/32
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
```
|
||||||
|
|
||||||
|
`<PRIVATE_KEY_SERVER_A>` - содержимое файла `private.key` с сервера **A**.\
|
||||||
|
`<PUBLIC_KEY_SERVER_B>` - содержимое файла `public.key` с сервера **B**.\
|
||||||
|
`<PUBLIC_IP_SERVER_B>` - публичный IP адресс сервера **B**.
|
||||||
|
|
||||||
|
#### Включаем туннель на обоих серверах:
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable --now awg-quick@awg0
|
||||||
|
```
|
||||||
|
|
||||||
|
Убедитесь, что с Сервера `A` доступен Сервер `B` через туннель.
|
||||||
|
```bash
|
||||||
|
ping 10.10.10.1
|
||||||
|
PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
|
||||||
|
64 bytes from 10.10.10.1: icmp_seq=1 ttl=64 time=35.1 ms
|
||||||
|
64 bytes from 10.10.10.1: icmp_seq=2 ttl=64 time=35.0 ms
|
||||||
|
64 bytes from 10.10.10.1: icmp_seq=3 ttl=64 time=35.1 ms
|
||||||
|
^C
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 2. Установка telemt на Сервере B (_условно Нидерланды_)
|
||||||
|
|
||||||
|
Установка и настройка описаны [здесь](https://github.com/telemt/telemt/blob/main/docs/QUICK_START_GUIDE.ru.md) или [здесь](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
|
||||||
|
Подразумевается что telemt ожидает подключения на порту `443\tcp`.
|
||||||
|
|
||||||
|
В конфиге telemt необходимо включить протокол `Proxy` и ограничить подключения к нему только через туннель.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[server]
|
||||||
|
port = 443
|
||||||
|
listen_addr_ipv4 = "10.10.10.1"
|
||||||
|
proxy_protocol = true
|
||||||
|
```
|
||||||
|
|
||||||
|
А также, для правильной генерации ссылок, указать FQDN или IP адрес и порт Сервера `A`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[general.links]
|
||||||
|
show = "*"
|
||||||
|
public_host = "<FQDN_OR_IP_SERVER_A>"
|
||||||
|
public_port = 443
|
||||||
|
```
|
||||||
|
|
||||||
|
Открываем порт на фаерволе (если включен):
|
||||||
|
```bash
|
||||||
|
sudo ufw allow from 10.10.10.2 to any port 443 proto tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Шаг 3. Настройка HAProxy на Сервере A (_РФ_)
|
||||||
|
|
||||||
|
Т.к. в стандартном репозитории Ubuntu версия относительно старая, имеет смысл воспользоваться официальным образом Docker.\
|
||||||
|
[Инструкция](https://docs.docker.com/engine/install/ubuntu/) по установке Docker на Ubuntu.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> По умолчанию у обычных пользователей нет прав на использование портов < 1024.\
|
||||||
|
> Попытки запустить HAProxy на 443 порту могут приводить к ошибкам:
|
||||||
|
> ```
|
||||||
|
> [ALERT] (8) : Binding [/usr/local/etc/haproxy/haproxy.cfg:17] for frontend tcp_in_443:
|
||||||
|
> protocol tcpv4: cannot bind socket (Permission denied) for [0.0.0.0:443].
|
||||||
|
> ```
|
||||||
|
> Есть два простых способа обойти это ограничение, выберите что-то одно:
|
||||||
|
> 1. На уровне ОС изменить настройку net.ipv4.ip_unprivileged_port_start, разрешив пользователям использовать все порты:
|
||||||
|
> ```
|
||||||
|
> echo "net.ipv4.ip_unprivileged_port_start = 0" | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
|
||||||
|
> ```
|
||||||
|
> или
|
||||||
|
>
|
||||||
|
> 2. Запустить HAProxy под root:\
|
||||||
|
> Раскомментируйте в docker-compose.yaml параметр `user: "root"`.
|
||||||
|
|
||||||
|
#### Создаем папку для HAProxy:
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/docker-compose/haproxy && cd $_
|
||||||
|
```
|
||||||
|
#### Создаем файл docker-compose.yaml
|
||||||
|
|
||||||
|
`nano docker-compose.yaml`
|
||||||
|
|
||||||
|
Содержимое файла
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
haproxy:
|
||||||
|
image: haproxy:latest
|
||||||
|
container_name: haproxy
|
||||||
|
restart: unless-stopped
|
||||||
|
# user: "root"
|
||||||
|
network_mode: "host"
|
||||||
|
volumes:
|
||||||
|
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
max-file: "1"
|
||||||
|
```
|
||||||
|
#### Создаем файл конфига haproxy.cfg
|
||||||
|
Принимаем подключения на порту 443\tcp и отправляем их через туннель на Сервер `B` 10.10.10.1:443
|
||||||
|
|
||||||
|
`nano haproxy.cfg`
|
||||||
|
|
||||||
|
Содержимое файла
|
||||||
|
```haproxy
|
||||||
|
global
|
||||||
|
log stdout format raw local0
|
||||||
|
maxconn 10000
|
||||||
|
|
||||||
|
defaults
|
||||||
|
log global
|
||||||
|
mode tcp
|
||||||
|
option tcplog
|
||||||
|
option clitcpka
|
||||||
|
option srvtcpka
|
||||||
|
timeout connect 5s
|
||||||
|
timeout client 2h
|
||||||
|
timeout server 2h
|
||||||
|
timeout check 5s
|
||||||
|
|
||||||
|
frontend tcp_in_443
|
||||||
|
bind *:443
|
||||||
|
maxconn 8000
|
||||||
|
option tcp-smart-accept
|
||||||
|
default_backend telemt_nodes
|
||||||
|
|
||||||
|
backend telemt_nodes
|
||||||
|
option tcp-smart-connect
|
||||||
|
server server_a 10.10.10.1:443 check inter 5s rise 2 fall 3 send-proxy-v2
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
>[!WARNING]
|
||||||
|
>**Файл должен заканчиваться пустой строкой, иначе HAProxy не запустится!**
|
||||||
|
|
||||||
|
#### Разрешаем порт 443\tcp в фаерволе (если включен)
|
||||||
|
```bash
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Запускаем контейнер HAProxy
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Если все настроено верно, то теперь можно пробовать подключить клиентов Telegram с использованием ссылок из лога\api telemt.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 650 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 838 KiB |
+546
-77
@@ -3,113 +3,582 @@ set -eu
|
|||||||
|
|
||||||
REPO="${REPO:-telemt/telemt}"
|
REPO="${REPO:-telemt/telemt}"
|
||||||
BIN_NAME="${BIN_NAME:-telemt}"
|
BIN_NAME="${BIN_NAME:-telemt}"
|
||||||
VERSION="${1:-${VERSION:-latest}}"
|
INSTALL_DIR="${INSTALL_DIR:-/bin}"
|
||||||
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
|
CONFIG_DIR="${CONFIG_DIR:-/etc/telemt}"
|
||||||
|
CONFIG_FILE="${CONFIG_FILE:-${CONFIG_DIR}/telemt.toml}"
|
||||||
|
WORK_DIR="${WORK_DIR:-/opt/telemt}"
|
||||||
|
TLS_DOMAIN="${TLS_DOMAIN:-petrovich.ru}"
|
||||||
|
SERVICE_NAME="telemt"
|
||||||
|
TEMP_DIR=""
|
||||||
|
SUDO=""
|
||||||
|
CONFIG_PARENT_DIR=""
|
||||||
|
SERVICE_START_FAILED=0
|
||||||
|
|
||||||
|
ACTION="install"
|
||||||
|
TARGET_VERSION="${VERSION:-latest}"
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help) ACTION="help"; shift ;;
|
||||||
|
-d|--domain)
|
||||||
|
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
|
||||||
|
printf '[ERROR] %s requires a domain argument.\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TLS_DOMAIN="$2"
|
||||||
|
shift 2 ;;
|
||||||
|
uninstall|--uninstall)
|
||||||
|
if [ "$ACTION" != "purge" ]; then ACTION="uninstall"; fi
|
||||||
|
shift ;;
|
||||||
|
purge|--purge) ACTION="purge"; shift ;;
|
||||||
|
install|--install) ACTION="install"; shift ;;
|
||||||
|
-*) printf '[ERROR] Unknown option: %s\n' "$1" >&2; exit 1 ;;
|
||||||
|
*)
|
||||||
|
if [ "$ACTION" = "install" ]; then TARGET_VERSION="$1"
|
||||||
|
else printf '[WARNING] Ignoring extra argument: %s\n' "$1" >&2; fi
|
||||||
|
shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
say() {
|
say() {
|
||||||
printf '%s\n' "$*"
|
if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '[INFO] %s\n' "$*"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
write_root() { $SUDO sh -c 'cat > "$1"' _ "$1"; }
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "${TEMP_DIR:-}" ] && [ -d "$TEMP_DIR" ]; then
|
||||||
|
rm -rf -- "$TEMP_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
say "Usage: $0 [ <version> | install | uninstall | purge ] [ -d <domain> ] [ --help ]"
|
||||||
|
say " <version> Install specific version (e.g. 3.3.15, default: latest)"
|
||||||
|
say " install Install the latest version"
|
||||||
|
say " uninstall Remove the binary and service (keeps config and user)"
|
||||||
|
say " purge Remove everything including configuration, data, and user"
|
||||||
|
say " -d, --domain Set TLS domain (default: petrovich.ru)"
|
||||||
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
die() {
|
check_os_entity() {
|
||||||
printf 'Error: %s\n' "$*" >&2
|
if command -v getent >/dev/null 2>&1; then getent "$1" "$2" >/dev/null 2>&1
|
||||||
exit 1
|
else grep -q "^${2}:" "/etc/$1" 2>/dev/null; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
need_cmd() {
|
normalize_path() {
|
||||||
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
printf '%s\n' "$1" | tr -s '/' | sed 's|/$||; s|^$|/|'
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_os() {
|
get_realpath() {
|
||||||
os="$(uname -s)"
|
path_in="$1"
|
||||||
case "$os" in
|
case "$path_in" in /*) ;; *) path_in="$(pwd)/$path_in" ;; esac
|
||||||
Linux) printf 'linux\n' ;;
|
|
||||||
OpenBSD) printf 'openbsd\n' ;;
|
if command -v realpath >/dev/null 2>&1; then
|
||||||
*) printf '%s\n' "$os" ;;
|
if realpath_out="$(realpath -m "$path_in" 2>/dev/null)"; then
|
||||||
|
printf '%s\n' "$realpath_out"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v readlink >/dev/null 2>&1; then
|
||||||
|
resolved_path="$(readlink -f "$path_in" 2>/dev/null || true)"
|
||||||
|
if [ -n "$resolved_path" ]; then
|
||||||
|
printf '%s\n' "$resolved_path"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
d="${path_in%/*}"; b="${path_in##*/}"
|
||||||
|
if [ -z "$d" ]; then d="/"; fi
|
||||||
|
if [ "$d" = "$path_in" ]; then d="/"; b="$path_in"; fi
|
||||||
|
|
||||||
|
if [ -d "$d" ]; then
|
||||||
|
abs_d="$(cd "$d" >/dev/null 2>&1 && pwd || true)"
|
||||||
|
if [ -n "$abs_d" ]; then
|
||||||
|
if [ "$b" = "." ] || [ -z "$b" ]; then printf '%s\n' "$abs_d"
|
||||||
|
elif [ "$abs_d" = "/" ]; then printf '/%s\n' "$b"
|
||||||
|
else printf '%s/%s\n' "$abs_d" "$b"; fi
|
||||||
|
else
|
||||||
|
normalize_path "$path_in"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
normalize_path "$path_in"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_svc_mgr() {
|
||||||
|
if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then echo "systemd"
|
||||||
|
elif command -v rc-service >/dev/null 2>&1; then echo "openrc"
|
||||||
|
else echo "none"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_common() {
|
||||||
|
[ -n "$BIN_NAME" ] || die "BIN_NAME cannot be empty."
|
||||||
|
[ -n "$INSTALL_DIR" ] || die "INSTALL_DIR cannot be empty."
|
||||||
|
[ -n "$CONFIG_DIR" ] || die "CONFIG_DIR cannot be empty."
|
||||||
|
[ -n "$CONFIG_FILE" ] || die "CONFIG_FILE cannot be empty."
|
||||||
|
|
||||||
|
case "${INSTALL_DIR}${CONFIG_DIR}${WORK_DIR}${CONFIG_FILE}" in
|
||||||
|
*[!a-zA-Z0-9_./-]*) die "Invalid characters in paths. Only alphanumeric, _, ., -, and / allowed." ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
case "$TARGET_VERSION" in *[!a-zA-Z0-9_.-]*) die "Invalid characters in version." ;; esac
|
||||||
|
case "$BIN_NAME" in *[!a-zA-Z0-9_-]*) die "Invalid characters in BIN_NAME." ;; esac
|
||||||
|
|
||||||
|
INSTALL_DIR="$(get_realpath "$INSTALL_DIR")"
|
||||||
|
CONFIG_DIR="$(get_realpath "$CONFIG_DIR")"
|
||||||
|
WORK_DIR="$(get_realpath "$WORK_DIR")"
|
||||||
|
CONFIG_FILE="$(get_realpath "$CONFIG_FILE")"
|
||||||
|
|
||||||
|
CONFIG_PARENT_DIR="${CONFIG_FILE%/*}"
|
||||||
|
if [ -z "$CONFIG_PARENT_DIR" ]; then CONFIG_PARENT_DIR="/"; fi
|
||||||
|
if [ "$CONFIG_PARENT_DIR" = "$CONFIG_FILE" ]; then CONFIG_PARENT_DIR="."; fi
|
||||||
|
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
SUDO=""
|
||||||
|
else
|
||||||
|
command -v sudo >/dev/null 2>&1 || die "This script requires root or sudo. Neither found."
|
||||||
|
SUDO="sudo"
|
||||||
|
if ! sudo -n true 2>/dev/null; then
|
||||||
|
if ! [ -t 0 ]; then
|
||||||
|
die "sudo requires a password, but no TTY detected. Aborting to prevent hang."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$SUDO" ]; then
|
||||||
|
if $SUDO sh -c '[ -d "$1" ]' _ "$CONFIG_FILE"; then
|
||||||
|
die "Safety check failed: CONFIG_FILE '$CONFIG_FILE' is a directory."
|
||||||
|
fi
|
||||||
|
elif [ -d "$CONFIG_FILE" ]; then
|
||||||
|
die "Safety check failed: CONFIG_FILE '$CONFIG_FILE' is a directory."
|
||||||
|
fi
|
||||||
|
|
||||||
|
for path in "$CONFIG_DIR" "$CONFIG_PARENT_DIR" "$WORK_DIR"; do
|
||||||
|
check_path="$(get_realpath "$path")"
|
||||||
|
case "$check_path" in
|
||||||
|
/|/bin|/sbin|/usr|/usr/bin|/usr/sbin|/usr/local|/usr/local/bin|/usr/local/sbin|/usr/local/etc|/usr/local/share|/etc|/var|/var/lib|/var/log|/var/run|/home|/root|/tmp|/lib|/lib64|/opt|/run|/boot|/dev|/sys|/proc)
|
||||||
|
die "Safety check failed: '$path' (resolved to '$check_path') is a critical system directory." ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
check_install_dir="$(get_realpath "$INSTALL_DIR")"
|
||||||
|
case "$check_install_dir" in
|
||||||
|
/|/etc|/var|/home|/root|/tmp|/usr|/usr/local|/opt|/boot|/dev|/sys|/proc|/run)
|
||||||
|
die "Safety check failed: INSTALL_DIR '$INSTALL_DIR' is a critical system directory." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for cmd in id uname grep find rm chown chmod mv mktemp mkdir tr dd sed ps head sleep cat tar gzip rmdir; do
|
||||||
|
command -v "$cmd" >/dev/null 2>&1 || die "Required command not found: $cmd"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_install_deps() {
|
||||||
|
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || die "Neither curl nor wget is installed."
|
||||||
|
command -v cp >/dev/null 2>&1 || command -v install >/dev/null 2>&1 || die "Need cp or install"
|
||||||
|
|
||||||
|
if ! command -v setcap >/dev/null 2>&1; then
|
||||||
|
if command -v apk >/dev/null 2>&1; then
|
||||||
|
$SUDO apk add --no-cache libcap-utils >/dev/null 2>&1 || $SUDO apk add --no-cache libcap >/dev/null 2>&1 || true
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
$SUDO apt-get update -q >/dev/null 2>&1 || true
|
||||||
|
$SUDO apt-get install -y -q libcap2-bin >/dev/null 2>&1 || true
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y -q libcap >/dev/null 2>&1 || true
|
||||||
|
elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y -q libcap >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_arch() {
|
detect_arch() {
|
||||||
arch="$(uname -m)"
|
sys_arch="$(uname -m)"
|
||||||
case "$arch" in
|
case "$sys_arch" in
|
||||||
x86_64|amd64) printf 'x86_64\n' ;;
|
x86_64|amd64)
|
||||||
aarch64|arm64) printf 'aarch64\n' ;;
|
if [ -r /proc/cpuinfo ] && grep -q "avx2" /proc/cpuinfo 2>/dev/null && grep -q "bmi2" /proc/cpuinfo 2>/dev/null; then
|
||||||
*) die "unsupported architecture: $arch" ;;
|
echo "x86_64-v3"
|
||||||
|
else
|
||||||
|
echo "x86_64"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
aarch64|arm64) echo "aarch64" ;;
|
||||||
|
*) die "Unsupported architecture: $sys_arch" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_libc() {
|
detect_libc() {
|
||||||
case "$(ldd --version 2>&1 || true)" in
|
for f in /lib/ld-musl-*.so.* /lib64/ld-musl-*.so.*; do
|
||||||
*musl*) printf 'musl\n' ;;
|
if [ -e "$f" ]; then echo "musl"; return 0; fi
|
||||||
*) printf 'gnu\n' ;;
|
done
|
||||||
esac
|
if grep -qE '^ID="?alpine"?' /etc/os-release 2>/dev/null; then echo "musl"; return 0; fi
|
||||||
|
if command -v ldd >/dev/null 2>&1 && (ldd --version 2>&1 || true) | grep -qi musl; then echo "musl"; return 0; fi
|
||||||
|
echo "gnu"
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch_to_stdout() {
|
fetch_file() {
|
||||||
url="$1"
|
if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2"
|
||||||
if command -v curl >/dev/null 2>&1; then
|
else wget -q -O "$2" "$1"; fi
|
||||||
curl -fsSL "$url"
|
}
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
|
||||||
wget -qO- "$url"
|
ensure_user_group() {
|
||||||
else
|
nologin_bin="$(command -v nologin 2>/dev/null || command -v false 2>/dev/null || echo /bin/false)"
|
||||||
die "neither curl nor wget is installed"
|
|
||||||
|
if ! check_os_entity group telemt; then
|
||||||
|
if command -v groupadd >/dev/null 2>&1; then $SUDO groupadd -r telemt
|
||||||
|
elif command -v addgroup >/dev/null 2>&1; then $SUDO addgroup -S telemt
|
||||||
|
else die "Cannot create group"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! check_os_entity passwd telemt; then
|
||||||
|
if command -v useradd >/dev/null 2>&1; then
|
||||||
|
$SUDO useradd -r -g telemt -d "$WORK_DIR" -s "$nologin_bin" -c "Telemt Proxy" telemt
|
||||||
|
elif command -v adduser >/dev/null 2>&1; then
|
||||||
|
if adduser --help 2>&1 | grep -q -- '-S'; then
|
||||||
|
$SUDO adduser -S -D -H -h "$WORK_DIR" -s "$nologin_bin" -G telemt telemt
|
||||||
|
else
|
||||||
|
$SUDO adduser --system --home "$WORK_DIR" --shell "$nologin_bin" --no-create-home --ingroup telemt --disabled-password telemt
|
||||||
|
fi
|
||||||
|
else die "Cannot create user"; fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_dirs() {
|
||||||
|
$SUDO mkdir -p "$WORK_DIR" "$CONFIG_DIR" "$CONFIG_PARENT_DIR" || die "Failed to create directories"
|
||||||
|
|
||||||
|
$SUDO chown telemt:telemt "$WORK_DIR" && $SUDO chmod 750 "$WORK_DIR"
|
||||||
|
$SUDO chown root:telemt "$CONFIG_DIR" && $SUDO chmod 750 "$CONFIG_DIR"
|
||||||
|
|
||||||
|
if [ "$CONFIG_PARENT_DIR" != "$CONFIG_DIR" ] && [ "$CONFIG_PARENT_DIR" != "." ] && [ "$CONFIG_PARENT_DIR" != "/" ]; then
|
||||||
|
$SUDO chown root:telemt "$CONFIG_PARENT_DIR" && $SUDO chmod 750 "$CONFIG_PARENT_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
svc="$(get_svc_mgr)"
|
||||||
|
if [ "$svc" = "systemd" ] && systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
|
||||||
|
$SUDO systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
elif [ "$svc" = "openrc" ] && rc-service "$SERVICE_NAME" status >/dev/null 2>&1; then
|
||||||
|
$SUDO rc-service "$SERVICE_NAME" stop 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_binary() {
|
install_binary() {
|
||||||
src="$1"
|
bin_src="$1"; bin_dst="$2"
|
||||||
dst="$2"
|
if [ -e "$INSTALL_DIR" ] && [ ! -d "$INSTALL_DIR" ]; then
|
||||||
|
die "'$INSTALL_DIR' is not a directory."
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -w "$INSTALL_DIR" ] || { [ ! -e "$INSTALL_DIR" ] && [ -w "$(dirname "$INSTALL_DIR")" ]; }; then
|
$SUDO mkdir -p "$INSTALL_DIR" || die "Failed to create install directory"
|
||||||
mkdir -p "$INSTALL_DIR"
|
if command -v install >/dev/null 2>&1; then
|
||||||
install -m 0755 "$src" "$dst"
|
$SUDO install -m 0755 "$bin_src" "$bin_dst" || die "Failed to install binary"
|
||||||
elif command -v sudo >/dev/null 2>&1; then
|
|
||||||
sudo mkdir -p "$INSTALL_DIR"
|
|
||||||
sudo install -m 0755 "$src" "$dst"
|
|
||||||
else
|
else
|
||||||
die "cannot write to $INSTALL_DIR and sudo is not available"
|
$SUDO rm -f "$bin_dst" 2>/dev/null || true
|
||||||
|
$SUDO cp "$bin_src" "$bin_dst" && $SUDO chmod 0755 "$bin_dst" || die "Failed to copy binary"
|
||||||
|
fi
|
||||||
|
|
||||||
|
$SUDO sh -c '[ -x "$1" ]' _ "$bin_dst" || die "Binary not executable: $bin_dst"
|
||||||
|
|
||||||
|
if command -v setcap >/dev/null 2>&1; then
|
||||||
|
$SUDO setcap cap_net_bind_service=+ep "$bin_dst" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
need_cmd uname
|
generate_secret() {
|
||||||
need_cmd tar
|
secret="$(command -v openssl >/dev/null 2>&1 && openssl rand -hex 16 2>/dev/null || true)"
|
||||||
need_cmd mktemp
|
if [ -z "$secret" ] || [ "${#secret}" -ne 32 ]; then
|
||||||
need_cmd grep
|
if command -v od >/dev/null 2>&1; then secret="$(dd if=/dev/urandom bs=16 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
||||||
need_cmd install
|
elif command -v hexdump >/dev/null 2>&1; then secret="$(dd if=/dev/urandom bs=16 count=1 2>/dev/null | hexdump -e '1/1 "%02x"')"
|
||||||
|
elif command -v xxd >/dev/null 2>&1; then secret="$(dd if=/dev/urandom bs=16 count=1 2>/dev/null | xxd -p | tr -d '\n')"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "${#secret}" -eq 32 ]; then echo "$secret"; else return 1; fi
|
||||||
|
}
|
||||||
|
|
||||||
ARCH="$(detect_arch)"
|
generate_config_content() {
|
||||||
OS="$(detect_os)"
|
escaped_tls_domain="$(printf '%s\n' "$TLS_DOMAIN" | tr -d '[:cntrl:]' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||||
|
|
||||||
if [ "$OS" != "linux" ]; then
|
cat <<EOF
|
||||||
case "$OS" in
|
[general]
|
||||||
openbsd)
|
use_middle_proxy = false
|
||||||
die "install.sh installs only Linux release artifacts. On OpenBSD, build from source (see docs/OPENBSD.en.md)."
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unsupported operating system for install.sh: $OS"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
LIBC="$(detect_libc)"
|
[general.modes]
|
||||||
|
classic = false
|
||||||
|
secure = false
|
||||||
|
tls = true
|
||||||
|
|
||||||
case "$VERSION" in
|
[server]
|
||||||
latest)
|
port = 443
|
||||||
URL="https://github.com/$REPO/releases/latest/download/${BIN_NAME}-${ARCH}-linux-${LIBC}.tar.gz"
|
|
||||||
;;
|
[server.api]
|
||||||
*)
|
enabled = true
|
||||||
URL="https://github.com/$REPO/releases/download/${VERSION}/${BIN_NAME}-${ARCH}-linux-${LIBC}.tar.gz"
|
listen = "127.0.0.1:9091"
|
||||||
|
whitelist = ["127.0.0.1/32"]
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "${escaped_tls_domain}"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
hello = "$1"
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
install_config() {
|
||||||
|
if [ -n "$SUDO" ]; then
|
||||||
|
if $SUDO sh -c '[ -f "$1" ]' _ "$CONFIG_FILE"; then
|
||||||
|
say " -> Config already exists at $CONFIG_FILE. Skipping creation."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
elif [ -f "$CONFIG_FILE" ]; then
|
||||||
|
say " -> Config already exists at $CONFIG_FILE. Skipping creation."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
toml_secret="$(generate_secret)" || die "Failed to generate secret."
|
||||||
|
|
||||||
|
generate_config_content "$toml_secret" | write_root "$CONFIG_FILE" || die "Failed to install config"
|
||||||
|
$SUDO chown root:telemt "$CONFIG_FILE" && $SUDO chmod 640 "$CONFIG_FILE"
|
||||||
|
|
||||||
|
say " -> Config created successfully."
|
||||||
|
say " -> Generated secret for default user 'hello': $toml_secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_systemd_content() {
|
||||||
|
cat <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Telemt
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=telemt
|
||||||
|
Group=telemt
|
||||||
|
WorkingDirectory=$WORK_DIR
|
||||||
|
ExecStart="${INSTALL_DIR}/${BIN_NAME}" "${CONFIG_FILE}"
|
||||||
|
Restart=on-failure
|
||||||
|
LimitNOFILE=65536
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_openrc_content() {
|
||||||
|
cat <<EOF
|
||||||
|
#!/sbin/openrc-run
|
||||||
|
name="$SERVICE_NAME"
|
||||||
|
description="Telemt Proxy Service"
|
||||||
|
command="${INSTALL_DIR}/${BIN_NAME}"
|
||||||
|
command_args="${CONFIG_FILE}"
|
||||||
|
command_background=true
|
||||||
|
command_user="telemt:telemt"
|
||||||
|
pidfile="/run/\${RC_SVCNAME}.pid"
|
||||||
|
directory="${WORK_DIR}"
|
||||||
|
rc_ulimit="-n 65536"
|
||||||
|
depend() { need net; use logger; }
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
install_service() {
|
||||||
|
svc="$(get_svc_mgr)"
|
||||||
|
if [ "$svc" = "systemd" ]; then
|
||||||
|
generate_systemd_content | write_root "/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
$SUDO chown root:root "/etc/systemd/system/${SERVICE_NAME}.service" && $SUDO chmod 644 "/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
|
||||||
|
$SUDO systemctl daemon-reload || true
|
||||||
|
$SUDO systemctl enable "$SERVICE_NAME" || true
|
||||||
|
|
||||||
|
if ! $SUDO systemctl start "$SERVICE_NAME"; then
|
||||||
|
say "[WARNING] Failed to start service"
|
||||||
|
SERVICE_START_FAILED=1
|
||||||
|
fi
|
||||||
|
elif [ "$svc" = "openrc" ]; then
|
||||||
|
generate_openrc_content | write_root "/etc/init.d/${SERVICE_NAME}"
|
||||||
|
$SUDO chown root:root "/etc/init.d/${SERVICE_NAME}" && $SUDO chmod 0755 "/etc/init.d/${SERVICE_NAME}"
|
||||||
|
|
||||||
|
$SUDO rc-update add "$SERVICE_NAME" default 2>/dev/null || true
|
||||||
|
|
||||||
|
if ! $SUDO rc-service "$SERVICE_NAME" start 2>/dev/null; then
|
||||||
|
say "[WARNING] Failed to start service"
|
||||||
|
SERVICE_START_FAILED=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
cmd="\"${INSTALL_DIR}/${BIN_NAME}\" \"${CONFIG_FILE}\""
|
||||||
|
if [ -n "$SUDO" ]; then
|
||||||
|
say " -> Service manager not found. Start manually: sudo -u telemt $cmd"
|
||||||
|
else
|
||||||
|
say " -> Service manager not found. Start manually: su -s /bin/sh telemt -c '$cmd'"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
kill_user_procs() {
|
||||||
|
if command -v pkill >/dev/null 2>&1; then
|
||||||
|
$SUDO pkill -u telemt "$BIN_NAME" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
$SUDO pkill -9 -u telemt "$BIN_NAME" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
if command -v pgrep >/dev/null 2>&1; then
|
||||||
|
pids="$(pgrep -u telemt 2>/dev/null || true)"
|
||||||
|
else
|
||||||
|
pids="$(ps -u telemt -o pid= 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
for pid in $pids; do
|
||||||
|
case "$pid" in ''|*[!0-9]*) continue ;; *) $SUDO kill "$pid" 2>/dev/null || true ;; esac
|
||||||
|
done
|
||||||
|
sleep 1
|
||||||
|
for pid in $pids; do
|
||||||
|
case "$pid" in ''|*[!0-9]*) continue ;; *) $SUDO kill -9 "$pid" 2>/dev/null || true ;; esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstall() {
|
||||||
|
say "Starting uninstallation of $BIN_NAME..."
|
||||||
|
|
||||||
|
say ">>> Stage 1: Stopping services"
|
||||||
|
stop_service
|
||||||
|
|
||||||
|
say ">>> Stage 2: Removing service configuration"
|
||||||
|
svc="$(get_svc_mgr)"
|
||||||
|
if [ "$svc" = "systemd" ]; then
|
||||||
|
$SUDO systemctl disable "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
$SUDO rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
$SUDO systemctl daemon-reload 2>/dev/null || true
|
||||||
|
elif [ "$svc" = "openrc" ]; then
|
||||||
|
$SUDO rc-update del "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
$SUDO rm -f "/etc/init.d/${SERVICE_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
say ">>> Stage 3: Terminating user processes"
|
||||||
|
kill_user_procs
|
||||||
|
|
||||||
|
say ">>> Stage 4: Removing binary"
|
||||||
|
$SUDO rm -f "${INSTALL_DIR}/${BIN_NAME}"
|
||||||
|
|
||||||
|
if [ "$ACTION" = "purge" ]; then
|
||||||
|
say ">>> Stage 5: Purging configuration, data, and user"
|
||||||
|
$SUDO rm -rf "$CONFIG_DIR" "$WORK_DIR"
|
||||||
|
$SUDO rm -f "$CONFIG_FILE"
|
||||||
|
if [ "$CONFIG_PARENT_DIR" != "$CONFIG_DIR" ] && [ "$CONFIG_PARENT_DIR" != "." ] && [ "$CONFIG_PARENT_DIR" != "/" ]; then
|
||||||
|
$SUDO rmdir "$CONFIG_PARENT_DIR" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
$SUDO userdel telemt 2>/dev/null || $SUDO deluser telemt 2>/dev/null || true
|
||||||
|
$SUDO groupdel telemt 2>/dev/null || $SUDO delgroup telemt 2>/dev/null || true
|
||||||
|
else
|
||||||
|
say "Note: Configuration and user kept. Run with 'purge' to remove completely."
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n====================================================================\n'
|
||||||
|
printf ' UNINSTALLATION COMPLETE\n'
|
||||||
|
printf '====================================================================\n\n'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
help) show_help ;;
|
||||||
|
uninstall|purge) verify_common; uninstall ;;
|
||||||
|
install)
|
||||||
|
say "Starting installation of $BIN_NAME (Version: $TARGET_VERSION)"
|
||||||
|
|
||||||
|
say ">>> Stage 1: Verifying environment and dependencies"
|
||||||
|
verify_common; verify_install_deps
|
||||||
|
|
||||||
|
if [ "$TARGET_VERSION" != "latest" ]; then
|
||||||
|
TARGET_VERSION="${TARGET_VERSION#v}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCH="$(detect_arch)"; LIBC="$(detect_libc)"
|
||||||
|
FILE_NAME="${BIN_NAME}-${ARCH}-linux-${LIBC}.tar.gz"
|
||||||
|
|
||||||
|
if [ "$TARGET_VERSION" = "latest" ]; then
|
||||||
|
DL_URL="https://github.com/${REPO}/releases/latest/download/${FILE_NAME}"
|
||||||
|
else
|
||||||
|
DL_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${FILE_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
say ">>> Stage 2: Downloading archive"
|
||||||
|
TEMP_DIR="$(mktemp -d)" || die "Temp directory creation failed"
|
||||||
|
if [ -z "$TEMP_DIR" ] || [ ! -d "$TEMP_DIR" ]; then
|
||||||
|
die "Temp directory is invalid or was not created"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! fetch_file "$DL_URL" "${TEMP_DIR}/${FILE_NAME}"; then
|
||||||
|
if [ "$ARCH" = "x86_64-v3" ]; then
|
||||||
|
say " -> x86_64-v3 build not found, falling back to standard x86_64..."
|
||||||
|
ARCH="x86_64"
|
||||||
|
FILE_NAME="${BIN_NAME}-${ARCH}-linux-${LIBC}.tar.gz"
|
||||||
|
if [ "$TARGET_VERSION" = "latest" ]; then
|
||||||
|
DL_URL="https://github.com/${REPO}/releases/latest/download/${FILE_NAME}"
|
||||||
|
else
|
||||||
|
DL_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${FILE_NAME}"
|
||||||
|
fi
|
||||||
|
fetch_file "$DL_URL" "${TEMP_DIR}/${FILE_NAME}" || die "Download failed"
|
||||||
|
else
|
||||||
|
die "Download failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
say ">>> Stage 3: Extracting archive"
|
||||||
|
if ! gzip -dc "${TEMP_DIR}/${FILE_NAME}" | tar -xf - -C "$TEMP_DIR" 2>/dev/null; then
|
||||||
|
die "Extraction failed (downloaded archive might be invalid or 404)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXTRACTED_BIN="$(find "$TEMP_DIR" -type f -name "$BIN_NAME" -print 2>/dev/null | head -n 1 || true)"
|
||||||
|
[ -n "$EXTRACTED_BIN" ] || die "Binary '$BIN_NAME' not found in archive"
|
||||||
|
|
||||||
|
say ">>> Stage 4: Setting up environment (User, Group, Directories)"
|
||||||
|
ensure_user_group; setup_dirs; stop_service
|
||||||
|
|
||||||
|
say ">>> Stage 5: Installing binary"
|
||||||
|
install_binary "$EXTRACTED_BIN" "${INSTALL_DIR}/${BIN_NAME}"
|
||||||
|
|
||||||
|
say ">>> Stage 6: Generating configuration"
|
||||||
|
install_config
|
||||||
|
|
||||||
|
say ">>> Stage 7: Installing and starting service"
|
||||||
|
install_service
|
||||||
|
|
||||||
|
if [ "${SERVICE_START_FAILED:-0}" -eq 1 ]; then
|
||||||
|
printf '\n====================================================================\n'
|
||||||
|
printf ' INSTALLATION COMPLETED WITH WARNINGS\n'
|
||||||
|
printf '====================================================================\n\n'
|
||||||
|
printf 'The service was installed but failed to start automatically.\n'
|
||||||
|
printf 'Please check the logs to determine the issue.\n\n'
|
||||||
|
else
|
||||||
|
printf '\n====================================================================\n'
|
||||||
|
printf ' INSTALLATION SUCCESS\n'
|
||||||
|
printf '====================================================================\n\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
svc="$(get_svc_mgr)"
|
||||||
|
if [ "$svc" = "systemd" ]; then
|
||||||
|
printf 'To check the status of your proxy service, run:\n'
|
||||||
|
printf ' systemctl status %s\n\n' "$SERVICE_NAME"
|
||||||
|
elif [ "$svc" = "openrc" ]; then
|
||||||
|
printf 'To check the status of your proxy service, run:\n'
|
||||||
|
printf ' rc-service %s status\n\n' "$SERVICE_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'To get your user connection links (for Telegram), run:\n'
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
printf ' curl -s http://127.0.0.1:9091/v1/users | jq -r '\''.data[] | "User: \\(.username)\\n\\(.links.tls[0] // empty)\\n"'\''\n'
|
||||||
|
else
|
||||||
|
printf ' curl -s http://127.0.0.1:9091/v1/users\n'
|
||||||
|
printf ' (Tip: Install '\''jq'\'' for a much cleaner output)\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n====================================================================\n'
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
TMPDIR="$(mktemp -d)"
|
|
||||||
trap 'rm -rf "$TMPDIR"' EXIT INT TERM
|
|
||||||
|
|
||||||
say "Installing $BIN_NAME ($VERSION) for $ARCH-linux-$LIBC..."
|
|
||||||
fetch_to_stdout "$URL" | tar -xzf - -C "$TMPDIR"
|
|
||||||
|
|
||||||
[ -f "$TMPDIR/$BIN_NAME" ] || die "archive did not contain $BIN_NAME"
|
|
||||||
|
|
||||||
install_binary "$TMPDIR/$BIN_NAME" "$INSTALL_DIR/$BIN_NAME"
|
|
||||||
|
|
||||||
say "Installed: $INSTALL_DIR/$BIN_NAME"
|
|
||||||
"$INSTALL_DIR/$BIN_NAME" --version 2>/dev/null || true
|
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ pub(super) fn success_response<T: Serialize>(
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn error_response(
|
pub(super) fn error_response(request_id: u64, failure: ApiFailure) -> hyper::Response<Full<Bytes>> {
|
||||||
request_id: u64,
|
|
||||||
failure: ApiFailure,
|
|
||||||
) -> hyper::Response<Full<Bytes>> {
|
|
||||||
let payload = ErrorResponse {
|
let payload = ErrorResponse {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: ErrorBody {
|
error: ErrorBody {
|
||||||
|
|||||||
+93
-31
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -19,8 +21,8 @@ use crate::ip_tracker::UserIpTracker;
|
|||||||
use crate::proxy::route_mode::RouteRuntimeController;
|
use crate::proxy::route_mode::RouteRuntimeController;
|
||||||
use crate::startup::StartupTracker;
|
use crate::startup::StartupTracker;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::transport::middle_proxy::MePool;
|
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
use crate::transport::middle_proxy::MePool;
|
||||||
|
|
||||||
mod config_store;
|
mod config_store;
|
||||||
mod events;
|
mod events;
|
||||||
@@ -35,11 +37,12 @@ mod runtime_watch;
|
|||||||
mod runtime_zero;
|
mod runtime_zero;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
use config_store::{current_revision, parse_if_match};
|
use config_store::{current_revision, load_config_from_disk, parse_if_match};
|
||||||
use http_utils::{error_response, read_json, read_optional_json, success_response};
|
|
||||||
use events::ApiEventStore;
|
use events::ApiEventStore;
|
||||||
|
use http_utils::{error_response, read_json, read_optional_json, success_response};
|
||||||
use model::{
|
use model::{
|
||||||
ApiFailure, CreateUserRequest, HealthData, PatchUserRequest, RotateSecretRequest, SummaryData,
|
ApiFailure, CreateUserRequest, DeleteUserResponse, HealthData, PatchUserRequest,
|
||||||
|
RotateSecretRequest, SummaryData, UserActiveIps,
|
||||||
};
|
};
|
||||||
use runtime_edge::{
|
use runtime_edge::{
|
||||||
EdgeConnectionsCacheEntry, build_runtime_connections_summary_data,
|
EdgeConnectionsCacheEntry, build_runtime_connections_summary_data,
|
||||||
@@ -55,11 +58,11 @@ use runtime_stats::{
|
|||||||
MinimalCacheEntry, build_dcs_data, build_me_writers_data, build_minimal_all_data,
|
MinimalCacheEntry, build_dcs_data, build_me_writers_data, build_minimal_all_data,
|
||||||
build_upstreams_data, build_zero_all_data,
|
build_upstreams_data, build_zero_all_data,
|
||||||
};
|
};
|
||||||
|
use runtime_watch::spawn_runtime_watchers;
|
||||||
use runtime_zero::{
|
use runtime_zero::{
|
||||||
build_limits_effective_data, build_runtime_gates_data, build_security_posture_data,
|
build_limits_effective_data, build_runtime_gates_data, build_security_posture_data,
|
||||||
build_system_info_data,
|
build_system_info_data,
|
||||||
};
|
};
|
||||||
use runtime_watch::spawn_runtime_watchers;
|
|
||||||
use users::{create_user, delete_user, patch_user, rotate_secret, users_from_config};
|
use users::{create_user, delete_user, patch_user, rotate_secret, users_from_config};
|
||||||
|
|
||||||
pub(super) struct ApiRuntimeState {
|
pub(super) struct ApiRuntimeState {
|
||||||
@@ -208,15 +211,15 @@ async fn handle(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !api_cfg.whitelist.is_empty()
|
if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip()))
|
||||||
&& !api_cfg
|
|
||||||
.whitelist
|
|
||||||
.iter()
|
|
||||||
.any(|net| net.contains(peer.ip()))
|
|
||||||
{
|
{
|
||||||
return Ok(error_response(
|
return Ok(error_response(
|
||||||
request_id,
|
request_id,
|
||||||
ApiFailure::new(StatusCode::FORBIDDEN, "forbidden", "Source IP is not allowed"),
|
ApiFailure::new(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"forbidden",
|
||||||
|
"Source IP is not allowed",
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +350,8 @@ async fn handle(
|
|||||||
}
|
}
|
||||||
("GET", "/v1/runtime/connections/summary") => {
|
("GET", "/v1/runtime/connections/summary") => {
|
||||||
let revision = current_revision(&shared.config_path).await?;
|
let revision = current_revision(&shared.config_path).await?;
|
||||||
let data = build_runtime_connections_summary_data(shared.as_ref(), cfg.as_ref()).await;
|
let data =
|
||||||
|
build_runtime_connections_summary_data(shared.as_ref(), cfg.as_ref()).await;
|
||||||
Ok(success_response(StatusCode::OK, data, revision))
|
Ok(success_response(StatusCode::OK, data, revision))
|
||||||
}
|
}
|
||||||
("GET", "/v1/runtime/events/recent") => {
|
("GET", "/v1/runtime/events/recent") => {
|
||||||
@@ -359,15 +363,33 @@ async fn handle(
|
|||||||
);
|
);
|
||||||
Ok(success_response(StatusCode::OK, data, revision))
|
Ok(success_response(StatusCode::OK, data, revision))
|
||||||
}
|
}
|
||||||
|
("GET", "/v1/stats/users/active-ips") => {
|
||||||
|
let revision = current_revision(&shared.config_path).await?;
|
||||||
|
let usernames: Vec<_> = cfg.access.users.keys().cloned().collect();
|
||||||
|
let active_ips_map = shared.ip_tracker.get_active_ips_for_users(&usernames).await;
|
||||||
|
let mut data: Vec<UserActiveIps> = active_ips_map
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, ips)| !ips.is_empty())
|
||||||
|
.map(|(username, active_ips)| UserActiveIps {
|
||||||
|
username,
|
||||||
|
active_ips,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
data.sort_by(|a, b| a.username.cmp(&b.username));
|
||||||
|
Ok(success_response(StatusCode::OK, data, revision))
|
||||||
|
}
|
||||||
("GET", "/v1/stats/users") | ("GET", "/v1/users") => {
|
("GET", "/v1/stats/users") | ("GET", "/v1/users") => {
|
||||||
let revision = current_revision(&shared.config_path).await?;
|
let revision = current_revision(&shared.config_path).await?;
|
||||||
|
let disk_cfg = load_config_from_disk(&shared.config_path).await?;
|
||||||
|
let runtime_cfg = config_rx.borrow().clone();
|
||||||
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
||||||
let users = users_from_config(
|
let users = users_from_config(
|
||||||
&cfg,
|
&disk_cfg,
|
||||||
&shared.stats,
|
&shared.stats,
|
||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
|
Some(runtime_cfg.as_ref()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Ok(success_response(StatusCode::OK, users, revision))
|
Ok(success_response(StatusCode::OK, users, revision))
|
||||||
@@ -386,17 +408,27 @@ async fn handle(
|
|||||||
let expected_revision = parse_if_match(req.headers());
|
let expected_revision = parse_if_match(req.headers());
|
||||||
let body = read_json::<CreateUserRequest>(req.into_body(), body_limit).await?;
|
let body = read_json::<CreateUserRequest>(req.into_body(), body_limit).await?;
|
||||||
let result = create_user(body, expected_revision, &shared).await;
|
let result = create_user(body, expected_revision, &shared).await;
|
||||||
let (data, revision) = match result {
|
let (mut data, revision) = match result {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
shared.runtime_events.record("api.user.create.failed", error.code);
|
shared
|
||||||
|
.runtime_events
|
||||||
|
.record("api.user.create.failed", error.code);
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
shared
|
let runtime_cfg = config_rx.borrow().clone();
|
||||||
.runtime_events
|
data.user.in_runtime = runtime_cfg.access.users.contains_key(&data.user.username);
|
||||||
.record("api.user.create.ok", format!("username={}", data.user.username));
|
shared.runtime_events.record(
|
||||||
Ok(success_response(StatusCode::CREATED, data, revision))
|
"api.user.create.ok",
|
||||||
|
format!("username={}", data.user.username),
|
||||||
|
);
|
||||||
|
let status = if data.user.in_runtime {
|
||||||
|
StatusCode::CREATED
|
||||||
|
} else {
|
||||||
|
StatusCode::ACCEPTED
|
||||||
|
};
|
||||||
|
Ok(success_response(status, data, revision))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(user) = path.strip_prefix("/v1/users/")
|
if let Some(user) = path.strip_prefix("/v1/users/")
|
||||||
@@ -405,16 +437,20 @@ async fn handle(
|
|||||||
{
|
{
|
||||||
if method == Method::GET {
|
if method == Method::GET {
|
||||||
let revision = current_revision(&shared.config_path).await?;
|
let revision = current_revision(&shared.config_path).await?;
|
||||||
|
let disk_cfg = load_config_from_disk(&shared.config_path).await?;
|
||||||
|
let runtime_cfg = config_rx.borrow().clone();
|
||||||
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
||||||
let users = users_from_config(
|
let users = users_from_config(
|
||||||
&cfg,
|
&disk_cfg,
|
||||||
&shared.stats,
|
&shared.stats,
|
||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
|
Some(runtime_cfg.as_ref()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if let Some(user_info) = users.into_iter().find(|entry| entry.username == user)
|
if let Some(user_info) =
|
||||||
|
users.into_iter().find(|entry| entry.username == user)
|
||||||
{
|
{
|
||||||
return Ok(success_response(StatusCode::OK, user_info, revision));
|
return Ok(success_response(StatusCode::OK, user_info, revision));
|
||||||
}
|
}
|
||||||
@@ -435,9 +471,10 @@ async fn handle(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let expected_revision = parse_if_match(req.headers());
|
let expected_revision = parse_if_match(req.headers());
|
||||||
let body = read_json::<PatchUserRequest>(req.into_body(), body_limit).await?;
|
let body =
|
||||||
|
read_json::<PatchUserRequest>(req.into_body(), body_limit).await?;
|
||||||
let result = patch_user(user, body, expected_revision, &shared).await;
|
let result = patch_user(user, body, expected_revision, &shared).await;
|
||||||
let (data, revision) = match result {
|
let (mut data, revision) = match result {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
shared.runtime_events.record(
|
shared.runtime_events.record(
|
||||||
@@ -447,10 +484,17 @@ async fn handle(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let runtime_cfg = config_rx.borrow().clone();
|
||||||
|
data.in_runtime = runtime_cfg.access.users.contains_key(&data.username);
|
||||||
shared
|
shared
|
||||||
.runtime_events
|
.runtime_events
|
||||||
.record("api.user.patch.ok", format!("username={}", data.username));
|
.record("api.user.patch.ok", format!("username={}", data.username));
|
||||||
return Ok(success_response(StatusCode::OK, data, revision));
|
let status = if data.in_runtime {
|
||||||
|
StatusCode::OK
|
||||||
|
} else {
|
||||||
|
StatusCode::ACCEPTED
|
||||||
|
};
|
||||||
|
return Ok(success_response(status, data, revision));
|
||||||
}
|
}
|
||||||
if method == Method::DELETE {
|
if method == Method::DELETE {
|
||||||
if api_cfg.read_only {
|
if api_cfg.read_only {
|
||||||
@@ -475,11 +519,21 @@ async fn handle(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
shared.runtime_events.record(
|
shared
|
||||||
"api.user.delete.ok",
|
.runtime_events
|
||||||
format!("username={}", deleted_user),
|
.record("api.user.delete.ok", format!("username={}", deleted_user));
|
||||||
);
|
let runtime_cfg = config_rx.borrow().clone();
|
||||||
return Ok(success_response(StatusCode::OK, deleted_user, revision));
|
let in_runtime = runtime_cfg.access.users.contains_key(&deleted_user);
|
||||||
|
let response = DeleteUserResponse {
|
||||||
|
username: deleted_user,
|
||||||
|
in_runtime,
|
||||||
|
};
|
||||||
|
let status = if response.in_runtime {
|
||||||
|
StatusCode::ACCEPTED
|
||||||
|
} else {
|
||||||
|
StatusCode::OK
|
||||||
|
};
|
||||||
|
return Ok(success_response(status, response, revision));
|
||||||
}
|
}
|
||||||
if method == Method::POST
|
if method == Method::POST
|
||||||
&& let Some(base_user) = user.strip_suffix("/rotate-secret")
|
&& let Some(base_user) = user.strip_suffix("/rotate-secret")
|
||||||
@@ -507,7 +561,7 @@ async fn handle(
|
|||||||
&shared,
|
&shared,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (data, revision) = match result {
|
let (mut data, revision) = match result {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
shared.runtime_events.record(
|
shared.runtime_events.record(
|
||||||
@@ -517,11 +571,19 @@ async fn handle(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let runtime_cfg = config_rx.borrow().clone();
|
||||||
|
data.user.in_runtime =
|
||||||
|
runtime_cfg.access.users.contains_key(&data.user.username);
|
||||||
shared.runtime_events.record(
|
shared.runtime_events.record(
|
||||||
"api.user.rotate_secret.ok",
|
"api.user.rotate_secret.ok",
|
||||||
format!("username={}", base_user),
|
format!("username={}", base_user),
|
||||||
);
|
);
|
||||||
return Ok(success_response(StatusCode::OK, data, revision));
|
let status = if data.user.in_runtime {
|
||||||
|
StatusCode::OK
|
||||||
|
} else {
|
||||||
|
StatusCode::ACCEPTED
|
||||||
|
};
|
||||||
|
return Ok(success_response(status, data, revision));
|
||||||
}
|
}
|
||||||
if method == Method::POST {
|
if method == Method::POST {
|
||||||
return Ok(error_response(
|
return Ok(error_response(
|
||||||
|
|||||||
+49
-2
@@ -1,10 +1,12 @@
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use rand::Rng;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::crypto::SecureRandom;
|
||||||
|
|
||||||
const MAX_USERNAME_LEN: usize = 64;
|
const MAX_USERNAME_LEN: usize = 64;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -79,10 +81,21 @@ pub(super) struct ZeroCoreData {
|
|||||||
pub(super) connections_total: u64,
|
pub(super) connections_total: u64,
|
||||||
pub(super) connections_bad_total: u64,
|
pub(super) connections_bad_total: u64,
|
||||||
pub(super) handshake_timeouts_total: u64,
|
pub(super) handshake_timeouts_total: u64,
|
||||||
|
pub(super) accept_permit_timeout_total: u64,
|
||||||
pub(super) configured_users: usize,
|
pub(super) configured_users: usize,
|
||||||
pub(super) telemetry_core_enabled: bool,
|
pub(super) telemetry_core_enabled: bool,
|
||||||
pub(super) telemetry_user_enabled: bool,
|
pub(super) telemetry_user_enabled: bool,
|
||||||
pub(super) telemetry_me_level: String,
|
pub(super) telemetry_me_level: String,
|
||||||
|
pub(super) conntrack_control_enabled: bool,
|
||||||
|
pub(super) conntrack_control_available: bool,
|
||||||
|
pub(super) conntrack_pressure_active: bool,
|
||||||
|
pub(super) conntrack_event_queue_depth: u64,
|
||||||
|
pub(super) conntrack_rule_apply_ok: bool,
|
||||||
|
pub(super) conntrack_delete_attempt_total: u64,
|
||||||
|
pub(super) conntrack_delete_success_total: u64,
|
||||||
|
pub(super) conntrack_delete_not_found_total: u64,
|
||||||
|
pub(super) conntrack_delete_error_total: u64,
|
||||||
|
pub(super) conntrack_close_event_drop_total: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
@@ -134,6 +147,7 @@ pub(super) struct UpstreamSummaryData {
|
|||||||
pub(super) direct_total: usize,
|
pub(super) direct_total: usize,
|
||||||
pub(super) socks4_total: usize,
|
pub(super) socks4_total: usize,
|
||||||
pub(super) socks5_total: usize,
|
pub(super) socks5_total: usize,
|
||||||
|
pub(super) shadowsocks_total: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
@@ -171,6 +185,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,
|
||||||
@@ -407,6 +439,7 @@ pub(super) struct UserLinks {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct UserInfo {
|
pub(super) struct UserInfo {
|
||||||
pub(super) username: String,
|
pub(super) username: String,
|
||||||
|
pub(super) in_runtime: bool,
|
||||||
pub(super) user_ad_tag: Option<String>,
|
pub(super) user_ad_tag: Option<String>,
|
||||||
pub(super) max_tcp_conns: Option<usize>,
|
pub(super) max_tcp_conns: Option<usize>,
|
||||||
pub(super) expiration_rfc3339: Option<String>,
|
pub(super) expiration_rfc3339: Option<String>,
|
||||||
@@ -421,12 +454,24 @@ pub(super) struct UserInfo {
|
|||||||
pub(super) links: UserLinks,
|
pub(super) links: UserLinks,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(super) struct UserActiveIps {
|
||||||
|
pub(super) username: String,
|
||||||
|
pub(super) active_ips: Vec<IpAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct CreateUserResponse {
|
pub(super) struct CreateUserResponse {
|
||||||
pub(super) user: UserInfo,
|
pub(super) user: UserInfo,
|
||||||
pub(super) secret: String,
|
pub(super) secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(super) struct DeleteUserResponse {
|
||||||
|
pub(super) username: String,
|
||||||
|
pub(super) in_runtime: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(super) struct CreateUserRequest {
|
pub(super) struct CreateUserRequest {
|
||||||
pub(super) username: String,
|
pub(super) username: String,
|
||||||
@@ -481,7 +526,9 @@ pub(super) fn is_valid_username(user: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn random_user_secret() -> String {
|
pub(super) fn random_user_secret() -> String {
|
||||||
|
static API_SECRET_RNG: OnceLock<SecureRandom> = OnceLock::new();
|
||||||
|
let rng = API_SECRET_RNG.get_or_init(SecureRandom::new);
|
||||||
let mut bytes = [0u8; 16];
|
let mut bytes = [0u8; 16];
|
||||||
rand::rng().fill(&mut bytes);
|
rng.fill(&mut bytes);
|
||||||
hex::encode(bytes)
|
hex::encode(bytes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,11 +167,7 @@ async fn current_me_pool_stage_progress(shared: &ApiShared) -> Option<f64> {
|
|||||||
let pool = shared.me_pool.read().await.clone()?;
|
let pool = shared.me_pool.read().await.clone()?;
|
||||||
let status = pool.api_status_snapshot().await;
|
let status = pool.api_status_snapshot().await;
|
||||||
let configured_dc_groups = status.configured_dc_groups;
|
let configured_dc_groups = status.configured_dc_groups;
|
||||||
let covered_dc_groups = status
|
let covered_dc_groups = status.dcs.iter().filter(|dc| dc.alive_writers > 0).count();
|
||||||
.dcs
|
|
||||||
.iter()
|
|
||||||
.filter(|dc| dc.alive_writers > 0)
|
|
||||||
.count();
|
|
||||||
|
|
||||||
let dc_coverage = ratio_01(covered_dc_groups, configured_dc_groups);
|
let dc_coverage = ratio_01(covered_dc_groups, configured_dc_groups);
|
||||||
let writer_coverage = ratio_01(status.alive_writers, status.required_writers);
|
let writer_coverage = ratio_01(status.alive_writers, status.required_writers);
|
||||||
|
|||||||
+62
-10
@@ -107,6 +107,25 @@ pub(super) struct RuntimeMeQualityRouteDropData {
|
|||||||
pub(super) queue_full_high_total: u64,
|
pub(super) queue_full_high_total: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(super) struct RuntimeMeQualityFamilyStateData {
|
||||||
|
pub(super) family: &'static str,
|
||||||
|
pub(super) state: &'static str,
|
||||||
|
pub(super) state_since_epoch_secs: u64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub(super) suppressed_until_epoch_secs: Option<u64>,
|
||||||
|
pub(super) fail_streak: u32,
|
||||||
|
pub(super) recover_success_streak: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(super) struct RuntimeMeQualityDrainGateData {
|
||||||
|
pub(super) route_quorum_ok: bool,
|
||||||
|
pub(super) redundancy_ok: bool,
|
||||||
|
pub(super) block_reason: &'static str,
|
||||||
|
pub(super) updated_at_epoch_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct RuntimeMeQualityDcRttData {
|
pub(super) struct RuntimeMeQualityDcRttData {
|
||||||
pub(super) dc: i16,
|
pub(super) dc: i16,
|
||||||
@@ -120,6 +139,8 @@ pub(super) struct RuntimeMeQualityDcRttData {
|
|||||||
pub(super) struct RuntimeMeQualityPayload {
|
pub(super) struct RuntimeMeQualityPayload {
|
||||||
pub(super) counters: RuntimeMeQualityCountersData,
|
pub(super) counters: RuntimeMeQualityCountersData,
|
||||||
pub(super) route_drops: RuntimeMeQualityRouteDropData,
|
pub(super) route_drops: RuntimeMeQualityRouteDropData,
|
||||||
|
pub(super) family_states: Vec<RuntimeMeQualityFamilyStateData>,
|
||||||
|
pub(super) drain_gate: RuntimeMeQualityDrainGateData,
|
||||||
pub(super) dc_rtt: Vec<RuntimeMeQualityDcRttData>,
|
pub(super) dc_rtt: Vec<RuntimeMeQualityDcRttData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +179,7 @@ pub(super) struct RuntimeUpstreamQualitySummaryData {
|
|||||||
pub(super) direct_total: usize,
|
pub(super) direct_total: usize,
|
||||||
pub(super) socks4_total: usize,
|
pub(super) socks4_total: usize,
|
||||||
pub(super) socks5_total: usize,
|
pub(super) socks5_total: usize,
|
||||||
|
pub(super) shadowsocks_total: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -360,6 +382,19 @@ pub(super) async fn build_runtime_me_quality_data(shared: &ApiShared) -> Runtime
|
|||||||
};
|
};
|
||||||
|
|
||||||
let status = pool.api_status_snapshot().await;
|
let status = pool.api_status_snapshot().await;
|
||||||
|
let family_states = pool
|
||||||
|
.api_family_state_snapshot()
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| RuntimeMeQualityFamilyStateData {
|
||||||
|
family: entry.family,
|
||||||
|
state: entry.state,
|
||||||
|
state_since_epoch_secs: entry.state_since_epoch_secs,
|
||||||
|
suppressed_until_epoch_secs: entry.suppressed_until_epoch_secs,
|
||||||
|
fail_streak: entry.fail_streak,
|
||||||
|
recover_success_streak: entry.recover_success_streak,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let drain_gate_snapshot = pool.api_drain_gate_snapshot();
|
||||||
RuntimeMeQualityData {
|
RuntimeMeQualityData {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
reason: None,
|
reason: None,
|
||||||
@@ -380,6 +415,13 @@ pub(super) async fn build_runtime_me_quality_data(shared: &ApiShared) -> Runtime
|
|||||||
queue_full_base_total: shared.stats.get_me_route_drop_queue_full_base(),
|
queue_full_base_total: shared.stats.get_me_route_drop_queue_full_base(),
|
||||||
queue_full_high_total: shared.stats.get_me_route_drop_queue_full_high(),
|
queue_full_high_total: shared.stats.get_me_route_drop_queue_full_high(),
|
||||||
},
|
},
|
||||||
|
family_states,
|
||||||
|
drain_gate: RuntimeMeQualityDrainGateData {
|
||||||
|
route_quorum_ok: drain_gate_snapshot.route_quorum_ok,
|
||||||
|
redundancy_ok: drain_gate_snapshot.redundancy_ok,
|
||||||
|
block_reason: drain_gate_snapshot.block_reason,
|
||||||
|
updated_at_epoch_secs: drain_gate_snapshot.updated_at_epoch_secs,
|
||||||
|
},
|
||||||
dc_rtt: status
|
dc_rtt: status
|
||||||
.dcs
|
.dcs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -404,7 +446,9 @@ pub(super) async fn build_runtime_upstream_quality_data(
|
|||||||
connect_attempt_total: shared.stats.get_upstream_connect_attempt_total(),
|
connect_attempt_total: shared.stats.get_upstream_connect_attempt_total(),
|
||||||
connect_success_total: shared.stats.get_upstream_connect_success_total(),
|
connect_success_total: shared.stats.get_upstream_connect_success_total(),
|
||||||
connect_fail_total: shared.stats.get_upstream_connect_fail_total(),
|
connect_fail_total: shared.stats.get_upstream_connect_fail_total(),
|
||||||
connect_failfast_hard_error_total: shared.stats.get_upstream_connect_failfast_hard_error_total(),
|
connect_failfast_hard_error_total: shared
|
||||||
|
.stats
|
||||||
|
.get_upstream_connect_failfast_hard_error_total(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(snapshot) = shared.upstream_manager.try_api_snapshot() else {
|
let Some(snapshot) = shared.upstream_manager.try_api_snapshot() else {
|
||||||
@@ -444,6 +488,7 @@ pub(super) async fn build_runtime_upstream_quality_data(
|
|||||||
direct_total: snapshot.summary.direct_total,
|
direct_total: snapshot.summary.direct_total,
|
||||||
socks4_total: snapshot.summary.socks4_total,
|
socks4_total: snapshot.summary.socks4_total,
|
||||||
socks5_total: snapshot.summary.socks5_total,
|
socks5_total: snapshot.summary.socks5_total,
|
||||||
|
shadowsocks_total: snapshot.summary.shadowsocks_total,
|
||||||
}),
|
}),
|
||||||
upstreams: Some(
|
upstreams: Some(
|
||||||
snapshot
|
snapshot
|
||||||
@@ -455,6 +500,7 @@ pub(super) async fn build_runtime_upstream_quality_data(
|
|||||||
crate::transport::UpstreamRouteKind::Direct => "direct",
|
crate::transport::UpstreamRouteKind::Direct => "direct",
|
||||||
crate::transport::UpstreamRouteKind::Socks4 => "socks4",
|
crate::transport::UpstreamRouteKind::Socks4 => "socks4",
|
||||||
crate::transport::UpstreamRouteKind::Socks5 => "socks5",
|
crate::transport::UpstreamRouteKind::Socks5 => "socks5",
|
||||||
|
crate::transport::UpstreamRouteKind::Shadowsocks => "shadowsocks",
|
||||||
},
|
},
|
||||||
address: upstream.address,
|
address: upstream.address,
|
||||||
weight: upstream.weight,
|
weight: upstream.weight,
|
||||||
@@ -474,7 +520,9 @@ pub(super) async fn build_runtime_upstream_quality_data(
|
|||||||
crate::transport::upstream::IpPreference::PreferV6 => "prefer_v6",
|
crate::transport::upstream::IpPreference::PreferV6 => "prefer_v6",
|
||||||
crate::transport::upstream::IpPreference::PreferV4 => "prefer_v4",
|
crate::transport::upstream::IpPreference::PreferV4 => "prefer_v4",
|
||||||
crate::transport::upstream::IpPreference::BothWork => "both_work",
|
crate::transport::upstream::IpPreference::BothWork => "both_work",
|
||||||
crate::transport::upstream::IpPreference::Unavailable => "unavailable",
|
crate::transport::upstream::IpPreference::Unavailable => {
|
||||||
|
"unavailable"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -512,14 +560,18 @@ pub(super) async fn build_runtime_nat_stun_data(shared: &ApiShared) -> RuntimeNa
|
|||||||
live_total: snapshot.live_servers.len(),
|
live_total: snapshot.live_servers.len(),
|
||||||
},
|
},
|
||||||
reflection: RuntimeNatStunReflectionBlockData {
|
reflection: RuntimeNatStunReflectionBlockData {
|
||||||
v4: snapshot.reflection_v4.map(|entry| RuntimeNatStunReflectionData {
|
v4: snapshot
|
||||||
addr: entry.addr.to_string(),
|
.reflection_v4
|
||||||
age_secs: entry.age_secs,
|
.map(|entry| RuntimeNatStunReflectionData {
|
||||||
}),
|
addr: entry.addr.to_string(),
|
||||||
v6: snapshot.reflection_v6.map(|entry| RuntimeNatStunReflectionData {
|
age_secs: entry.age_secs,
|
||||||
addr: entry.addr.to_string(),
|
}),
|
||||||
age_secs: entry.age_secs,
|
v6: snapshot
|
||||||
}),
|
.reflection_v6
|
||||||
|
.map(|entry| RuntimeNatStunReflectionData {
|
||||||
|
addr: entry.addr.to_string(),
|
||||||
|
age_secs: entry.age_secs,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
stun_backoff_remaining_ms: snapshot.stun_backoff_remaining_ms,
|
stun_backoff_remaining_ms: snapshot.stun_backoff_remaining_ms,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::net::IpAddr;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::net::IpAddr;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@ use serde::Serialize;
|
|||||||
|
|
||||||
use crate::config::{ProxyConfig, UpstreamType};
|
use crate::config::{ProxyConfig, UpstreamType};
|
||||||
use crate::network::probe::{detect_interface_ipv4, detect_interface_ipv6, is_bogon};
|
use crate::network::probe::{detect_interface_ipv4, detect_interface_ipv6, is_bogon};
|
||||||
use crate::transport::middle_proxy::{bnd_snapshot, timeskew_snapshot, upstream_bnd_snapshots};
|
|
||||||
use crate::transport::UpstreamRouteKind;
|
use crate::transport::UpstreamRouteKind;
|
||||||
|
use crate::transport::middle_proxy::{bnd_snapshot, timeskew_snapshot, upstream_bnd_snapshots};
|
||||||
|
|
||||||
use super::ApiShared;
|
use super::ApiShared;
|
||||||
|
|
||||||
@@ -262,8 +262,8 @@ fn update_kdf_ewma(now_epoch_secs: u64, total_errors: u64) -> f64 {
|
|||||||
let delta_errors = total_errors.saturating_sub(guard.last_total_errors);
|
let delta_errors = total_errors.saturating_sub(guard.last_total_errors);
|
||||||
let instant_rate_per_min = (delta_errors as f64) * 60.0 / (dt_secs as f64);
|
let instant_rate_per_min = (delta_errors as f64) * 60.0 / (dt_secs as f64);
|
||||||
let alpha = 1.0 - f64::exp(-(dt_secs as f64) / KDF_EWMA_TAU_SECS);
|
let alpha = 1.0 - f64::exp(-(dt_secs as f64) / KDF_EWMA_TAU_SECS);
|
||||||
guard.ewma_errors_per_min = guard.ewma_errors_per_min
|
guard.ewma_errors_per_min =
|
||||||
+ alpha * (instant_rate_per_min - guard.ewma_errors_per_min);
|
guard.ewma_errors_per_min + alpha * (instant_rate_per_min - guard.ewma_errors_per_min);
|
||||||
guard.last_epoch_secs = now_epoch_secs;
|
guard.last_epoch_secs = now_epoch_secs;
|
||||||
guard.last_total_errors = total_errors;
|
guard.last_total_errors = total_errors;
|
||||||
guard.ewma_errors_per_min
|
guard.ewma_errors_per_min
|
||||||
@@ -284,6 +284,7 @@ fn map_route_kind(value: UpstreamRouteKind) -> &'static str {
|
|||||||
UpstreamRouteKind::Direct => "direct",
|
UpstreamRouteKind::Direct => "direct",
|
||||||
UpstreamRouteKind::Socks4 => "socks4",
|
UpstreamRouteKind::Socks4 => "socks4",
|
||||||
UpstreamRouteKind::Socks5 => "socks5",
|
UpstreamRouteKind::Socks5 => "socks5",
|
||||||
|
UpstreamRouteKind::Shadowsocks => "shadowsocks",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-10
@@ -2,8 +2,8 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
use crate::config::ApiConfig;
|
use crate::config::ApiConfig;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::transport::upstream::IpPreference;
|
|
||||||
use crate::transport::UpstreamRouteKind;
|
use crate::transport::UpstreamRouteKind;
|
||||||
|
use crate::transport::upstream::IpPreference;
|
||||||
|
|
||||||
use super::ApiShared;
|
use super::ApiShared;
|
||||||
use super::model::{
|
use super::model::{
|
||||||
@@ -39,10 +39,21 @@ pub(super) fn build_zero_all_data(stats: &Stats, configured_users: usize) -> Zer
|
|||||||
connections_total: stats.get_connects_all(),
|
connections_total: stats.get_connects_all(),
|
||||||
connections_bad_total: stats.get_connects_bad(),
|
connections_bad_total: stats.get_connects_bad(),
|
||||||
handshake_timeouts_total: stats.get_handshake_timeouts(),
|
handshake_timeouts_total: stats.get_handshake_timeouts(),
|
||||||
|
accept_permit_timeout_total: stats.get_accept_permit_timeout_total(),
|
||||||
configured_users,
|
configured_users,
|
||||||
telemetry_core_enabled: telemetry.core_enabled,
|
telemetry_core_enabled: telemetry.core_enabled,
|
||||||
telemetry_user_enabled: telemetry.user_enabled,
|
telemetry_user_enabled: telemetry.user_enabled,
|
||||||
telemetry_me_level: telemetry.me_level.to_string(),
|
telemetry_me_level: telemetry.me_level.to_string(),
|
||||||
|
conntrack_control_enabled: stats.get_conntrack_control_enabled(),
|
||||||
|
conntrack_control_available: stats.get_conntrack_control_available(),
|
||||||
|
conntrack_pressure_active: stats.get_conntrack_pressure_active(),
|
||||||
|
conntrack_event_queue_depth: stats.get_conntrack_event_queue_depth(),
|
||||||
|
conntrack_rule_apply_ok: stats.get_conntrack_rule_apply_ok(),
|
||||||
|
conntrack_delete_attempt_total: stats.get_conntrack_delete_attempt_total(),
|
||||||
|
conntrack_delete_success_total: stats.get_conntrack_delete_success_total(),
|
||||||
|
conntrack_delete_not_found_total: stats.get_conntrack_delete_not_found_total(),
|
||||||
|
conntrack_delete_error_total: stats.get_conntrack_delete_error_total(),
|
||||||
|
conntrack_close_event_drop_total: stats.get_conntrack_close_event_drop_total(),
|
||||||
},
|
},
|
||||||
upstream: build_zero_upstream_data(stats),
|
upstream: build_zero_upstream_data(stats),
|
||||||
middle_proxy: ZeroMiddleProxyData {
|
middle_proxy: ZeroMiddleProxyData {
|
||||||
@@ -68,6 +79,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(),
|
||||||
@@ -136,7 +166,8 @@ fn build_zero_upstream_data(stats: &Stats) -> ZeroUpstreamData {
|
|||||||
.get_upstream_connect_duration_success_bucket_501_1000ms(),
|
.get_upstream_connect_duration_success_bucket_501_1000ms(),
|
||||||
connect_duration_success_bucket_gt_1000ms: stats
|
connect_duration_success_bucket_gt_1000ms: stats
|
||||||
.get_upstream_connect_duration_success_bucket_gt_1000ms(),
|
.get_upstream_connect_duration_success_bucket_gt_1000ms(),
|
||||||
connect_duration_fail_bucket_le_100ms: stats.get_upstream_connect_duration_fail_bucket_le_100ms(),
|
connect_duration_fail_bucket_le_100ms: stats
|
||||||
|
.get_upstream_connect_duration_fail_bucket_le_100ms(),
|
||||||
connect_duration_fail_bucket_101_500ms: stats
|
connect_duration_fail_bucket_101_500ms: stats
|
||||||
.get_upstream_connect_duration_fail_bucket_101_500ms(),
|
.get_upstream_connect_duration_fail_bucket_101_500ms(),
|
||||||
connect_duration_fail_bucket_501_1000ms: stats
|
connect_duration_fail_bucket_501_1000ms: stats
|
||||||
@@ -178,6 +209,7 @@ pub(super) fn build_upstreams_data(shared: &ApiShared, api_cfg: &ApiConfig) -> U
|
|||||||
direct_total: snapshot.summary.direct_total,
|
direct_total: snapshot.summary.direct_total,
|
||||||
socks4_total: snapshot.summary.socks4_total,
|
socks4_total: snapshot.summary.socks4_total,
|
||||||
socks5_total: snapshot.summary.socks5_total,
|
socks5_total: snapshot.summary.socks5_total,
|
||||||
|
shadowsocks_total: snapshot.summary.shadowsocks_total,
|
||||||
};
|
};
|
||||||
let upstreams = snapshot
|
let upstreams = snapshot
|
||||||
.upstreams
|
.upstreams
|
||||||
@@ -391,8 +423,7 @@ async fn get_minimal_payload_cached(
|
|||||||
adaptive_floor_min_writers_multi_endpoint: runtime
|
adaptive_floor_min_writers_multi_endpoint: runtime
|
||||||
.adaptive_floor_min_writers_multi_endpoint,
|
.adaptive_floor_min_writers_multi_endpoint,
|
||||||
adaptive_floor_recover_grace_secs: runtime.adaptive_floor_recover_grace_secs,
|
adaptive_floor_recover_grace_secs: runtime.adaptive_floor_recover_grace_secs,
|
||||||
adaptive_floor_writers_per_core_total: runtime
|
adaptive_floor_writers_per_core_total: runtime.adaptive_floor_writers_per_core_total,
|
||||||
.adaptive_floor_writers_per_core_total,
|
|
||||||
adaptive_floor_cpu_cores_override: runtime.adaptive_floor_cpu_cores_override,
|
adaptive_floor_cpu_cores_override: runtime.adaptive_floor_cpu_cores_override,
|
||||||
adaptive_floor_max_extra_writers_single_per_core: runtime
|
adaptive_floor_max_extra_writers_single_per_core: runtime
|
||||||
.adaptive_floor_max_extra_writers_single_per_core,
|
.adaptive_floor_max_extra_writers_single_per_core,
|
||||||
@@ -400,12 +431,9 @@ async fn get_minimal_payload_cached(
|
|||||||
.adaptive_floor_max_extra_writers_multi_per_core,
|
.adaptive_floor_max_extra_writers_multi_per_core,
|
||||||
adaptive_floor_max_active_writers_per_core: runtime
|
adaptive_floor_max_active_writers_per_core: runtime
|
||||||
.adaptive_floor_max_active_writers_per_core,
|
.adaptive_floor_max_active_writers_per_core,
|
||||||
adaptive_floor_max_warm_writers_per_core: runtime
|
adaptive_floor_max_warm_writers_per_core: runtime.adaptive_floor_max_warm_writers_per_core,
|
||||||
.adaptive_floor_max_warm_writers_per_core,
|
adaptive_floor_max_active_writers_global: runtime.adaptive_floor_max_active_writers_global,
|
||||||
adaptive_floor_max_active_writers_global: runtime
|
adaptive_floor_max_warm_writers_global: runtime.adaptive_floor_max_warm_writers_global,
|
||||||
.adaptive_floor_max_active_writers_global,
|
|
||||||
adaptive_floor_max_warm_writers_global: runtime
|
|
||||||
.adaptive_floor_max_warm_writers_global,
|
|
||||||
adaptive_floor_cpu_cores_detected: runtime.adaptive_floor_cpu_cores_detected,
|
adaptive_floor_cpu_cores_detected: runtime.adaptive_floor_cpu_cores_detected,
|
||||||
adaptive_floor_cpu_cores_effective: runtime.adaptive_floor_cpu_cores_effective,
|
adaptive_floor_cpu_cores_effective: runtime.adaptive_floor_cpu_cores_effective,
|
||||||
adaptive_floor_global_cap_raw: runtime.adaptive_floor_global_cap_raw,
|
adaptive_floor_global_cap_raw: runtime.adaptive_floor_global_cap_raw,
|
||||||
@@ -517,6 +545,7 @@ fn map_route_kind(value: UpstreamRouteKind) -> &'static str {
|
|||||||
UpstreamRouteKind::Direct => "direct",
|
UpstreamRouteKind::Direct => "direct",
|
||||||
UpstreamRouteKind::Socks4 => "socks4",
|
UpstreamRouteKind::Socks4 => "socks4",
|
||||||
UpstreamRouteKind::Socks5 => "socks5",
|
UpstreamRouteKind::Socks5 => "socks5",
|
||||||
|
UpstreamRouteKind::Shadowsocks => "shadowsocks",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-6
@@ -35,11 +35,14 @@ pub(super) struct RuntimeGatesData {
|
|||||||
pub(super) conditional_cast_enabled: bool,
|
pub(super) conditional_cast_enabled: bool,
|
||||||
pub(super) me_runtime_ready: bool,
|
pub(super) me_runtime_ready: bool,
|
||||||
pub(super) me2dc_fallback_enabled: bool,
|
pub(super) me2dc_fallback_enabled: bool,
|
||||||
|
pub(super) me2dc_fast_enabled: bool,
|
||||||
pub(super) use_middle_proxy: bool,
|
pub(super) use_middle_proxy: bool,
|
||||||
pub(super) route_mode: &'static str,
|
pub(super) route_mode: &'static str,
|
||||||
pub(super) reroute_active: bool,
|
pub(super) reroute_active: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub(super) reroute_to_direct_at_epoch_secs: Option<u64>,
|
pub(super) reroute_to_direct_at_epoch_secs: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub(super) reroute_reason: Option<&'static str>,
|
||||||
pub(super) startup_status: &'static str,
|
pub(super) startup_status: &'static str,
|
||||||
pub(super) startup_stage: String,
|
pub(super) startup_stage: String,
|
||||||
pub(super) startup_progress_pct: f64,
|
pub(super) startup_progress_pct: f64,
|
||||||
@@ -47,6 +50,7 @@ pub(super) struct RuntimeGatesData {
|
|||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct EffectiveTimeoutLimits {
|
pub(super) struct EffectiveTimeoutLimits {
|
||||||
|
pub(super) client_first_byte_idle_secs: u64,
|
||||||
pub(super) client_handshake_secs: u64,
|
pub(super) client_handshake_secs: u64,
|
||||||
pub(super) tg_connect_secs: u64,
|
pub(super) tg_connect_secs: u64,
|
||||||
pub(super) client_keepalive_secs: u64,
|
pub(super) client_keepalive_secs: u64,
|
||||||
@@ -86,6 +90,7 @@ pub(super) struct EffectiveMiddleProxyLimits {
|
|||||||
pub(super) writer_pick_mode: &'static str,
|
pub(super) writer_pick_mode: &'static str,
|
||||||
pub(super) writer_pick_sample_size: u8,
|
pub(super) writer_pick_sample_size: u8,
|
||||||
pub(super) me2dc_fallback: bool,
|
pub(super) me2dc_fallback: bool,
|
||||||
|
pub(super) me2dc_fast: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -95,6 +100,11 @@ pub(super) struct EffectiveUserIpPolicyLimits {
|
|||||||
pub(super) window_secs: u64,
|
pub(super) window_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(super) struct EffectiveUserTcpPolicyLimits {
|
||||||
|
pub(super) global_each: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct EffectiveLimitsData {
|
pub(super) struct EffectiveLimitsData {
|
||||||
pub(super) update_every_secs: u64,
|
pub(super) update_every_secs: u64,
|
||||||
@@ -104,6 +114,7 @@ pub(super) struct EffectiveLimitsData {
|
|||||||
pub(super) upstream: EffectiveUpstreamLimits,
|
pub(super) upstream: EffectiveUpstreamLimits,
|
||||||
pub(super) middle_proxy: EffectiveMiddleProxyLimits,
|
pub(super) middle_proxy: EffectiveMiddleProxyLimits,
|
||||||
pub(super) user_ip_policy: EffectiveUserIpPolicyLimits,
|
pub(super) user_ip_policy: EffectiveUserIpPolicyLimits,
|
||||||
|
pub(super) user_tcp_policy: EffectiveUserTcpPolicyLimits,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -128,7 +139,8 @@ pub(super) fn build_system_info_data(
|
|||||||
.runtime_state
|
.runtime_state
|
||||||
.last_config_reload_epoch_secs
|
.last_config_reload_epoch_secs
|
||||||
.load(Ordering::Relaxed);
|
.load(Ordering::Relaxed);
|
||||||
let last_config_reload_epoch_secs = (last_reload_epoch_secs > 0).then_some(last_reload_epoch_secs);
|
let last_config_reload_epoch_secs =
|
||||||
|
(last_reload_epoch_secs > 0).then_some(last_reload_epoch_secs);
|
||||||
|
|
||||||
let git_commit = option_env!("TELEMT_GIT_COMMIT")
|
let git_commit = option_env!("TELEMT_GIT_COMMIT")
|
||||||
.or(option_env!("VERGEN_GIT_SHA"))
|
.or(option_env!("VERGEN_GIT_SHA"))
|
||||||
@@ -153,7 +165,10 @@ pub(super) fn build_system_info_data(
|
|||||||
uptime_seconds: shared.stats.uptime_secs(),
|
uptime_seconds: shared.stats.uptime_secs(),
|
||||||
config_path: shared.config_path.display().to_string(),
|
config_path: shared.config_path.display().to_string(),
|
||||||
config_hash: revision.to_string(),
|
config_hash: revision.to_string(),
|
||||||
config_reload_count: shared.runtime_state.config_reload_count.load(Ordering::Relaxed),
|
config_reload_count: shared
|
||||||
|
.runtime_state
|
||||||
|
.config_reload_count
|
||||||
|
.load(Ordering::Relaxed),
|
||||||
last_config_reload_epoch_secs,
|
last_config_reload_epoch_secs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,6 +180,8 @@ pub(super) async fn build_runtime_gates_data(
|
|||||||
let startup_summary = build_runtime_startup_summary(shared).await;
|
let startup_summary = build_runtime_startup_summary(shared).await;
|
||||||
let route_state = shared.route_runtime.snapshot();
|
let route_state = shared.route_runtime.snapshot();
|
||||||
let route_mode = route_state.mode.as_str();
|
let route_mode = route_state.mode.as_str();
|
||||||
|
let fast_fallback_enabled =
|
||||||
|
cfg.general.use_middle_proxy && cfg.general.me2dc_fallback && cfg.general.me2dc_fast;
|
||||||
let reroute_active = cfg.general.use_middle_proxy
|
let reroute_active = cfg.general.use_middle_proxy
|
||||||
&& cfg.general.me2dc_fallback
|
&& cfg.general.me2dc_fallback
|
||||||
&& matches!(route_state.mode, RelayRouteMode::Direct);
|
&& matches!(route_state.mode, RelayRouteMode::Direct);
|
||||||
@@ -173,6 +190,15 @@ pub(super) async fn build_runtime_gates_data(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
let reroute_reason = if reroute_active {
|
||||||
|
if fast_fallback_enabled {
|
||||||
|
Some("fast_not_ready_fallback")
|
||||||
|
} else {
|
||||||
|
Some("strict_grace_fallback")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let me_runtime_ready = if !cfg.general.use_middle_proxy {
|
let me_runtime_ready = if !cfg.general.use_middle_proxy {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -190,10 +216,12 @@ pub(super) async fn build_runtime_gates_data(
|
|||||||
conditional_cast_enabled: cfg.general.use_middle_proxy,
|
conditional_cast_enabled: cfg.general.use_middle_proxy,
|
||||||
me_runtime_ready,
|
me_runtime_ready,
|
||||||
me2dc_fallback_enabled: cfg.general.me2dc_fallback,
|
me2dc_fallback_enabled: cfg.general.me2dc_fallback,
|
||||||
|
me2dc_fast_enabled: fast_fallback_enabled,
|
||||||
use_middle_proxy: cfg.general.use_middle_proxy,
|
use_middle_proxy: cfg.general.use_middle_proxy,
|
||||||
route_mode,
|
route_mode,
|
||||||
reroute_active,
|
reroute_active,
|
||||||
reroute_to_direct_at_epoch_secs,
|
reroute_to_direct_at_epoch_secs,
|
||||||
|
reroute_reason,
|
||||||
startup_status: startup_summary.status,
|
startup_status: startup_summary.status,
|
||||||
startup_stage: startup_summary.stage,
|
startup_stage: startup_summary.stage,
|
||||||
startup_progress_pct: startup_summary.progress_pct,
|
startup_progress_pct: startup_summary.progress_pct,
|
||||||
@@ -206,8 +234,9 @@ pub(super) fn build_limits_effective_data(cfg: &ProxyConfig) -> EffectiveLimitsD
|
|||||||
me_reinit_every_secs: cfg.general.effective_me_reinit_every_secs(),
|
me_reinit_every_secs: cfg.general.effective_me_reinit_every_secs(),
|
||||||
me_pool_force_close_secs: cfg.general.effective_me_pool_force_close_secs(),
|
me_pool_force_close_secs: cfg.general.effective_me_pool_force_close_secs(),
|
||||||
timeouts: EffectiveTimeoutLimits {
|
timeouts: EffectiveTimeoutLimits {
|
||||||
|
client_first_byte_idle_secs: cfg.timeouts.client_first_byte_idle_secs,
|
||||||
client_handshake_secs: cfg.timeouts.client_handshake,
|
client_handshake_secs: cfg.timeouts.client_handshake,
|
||||||
tg_connect_secs: cfg.timeouts.tg_connect,
|
tg_connect_secs: cfg.general.tg_connect,
|
||||||
client_keepalive_secs: cfg.timeouts.client_keepalive,
|
client_keepalive_secs: cfg.timeouts.client_keepalive,
|
||||||
client_ack_secs: cfg.timeouts.client_ack,
|
client_ack_secs: cfg.timeouts.client_ack,
|
||||||
me_one_retry: cfg.timeouts.me_one_retry,
|
me_one_retry: cfg.timeouts.me_one_retry,
|
||||||
@@ -233,9 +262,7 @@ pub(super) fn build_limits_effective_data(cfg: &ProxyConfig) -> EffectiveLimitsD
|
|||||||
adaptive_floor_writers_per_core_total: cfg
|
adaptive_floor_writers_per_core_total: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_writers_per_core_total,
|
.me_adaptive_floor_writers_per_core_total,
|
||||||
adaptive_floor_cpu_cores_override: cfg
|
adaptive_floor_cpu_cores_override: cfg.general.me_adaptive_floor_cpu_cores_override,
|
||||||
.general
|
|
||||||
.me_adaptive_floor_cpu_cores_override,
|
|
||||||
adaptive_floor_max_extra_writers_single_per_core: cfg
|
adaptive_floor_max_extra_writers_single_per_core: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_max_extra_writers_single_per_core,
|
.me_adaptive_floor_max_extra_writers_single_per_core,
|
||||||
@@ -261,12 +288,16 @@ pub(super) fn build_limits_effective_data(cfg: &ProxyConfig) -> EffectiveLimitsD
|
|||||||
writer_pick_mode: me_writer_pick_mode_label(cfg.general.me_writer_pick_mode),
|
writer_pick_mode: me_writer_pick_mode_label(cfg.general.me_writer_pick_mode),
|
||||||
writer_pick_sample_size: cfg.general.me_writer_pick_sample_size,
|
writer_pick_sample_size: cfg.general.me_writer_pick_sample_size,
|
||||||
me2dc_fallback: cfg.general.me2dc_fallback,
|
me2dc_fallback: cfg.general.me2dc_fallback,
|
||||||
|
me2dc_fast: cfg.general.me2dc_fast,
|
||||||
},
|
},
|
||||||
user_ip_policy: EffectiveUserIpPolicyLimits {
|
user_ip_policy: EffectiveUserIpPolicyLimits {
|
||||||
global_each: cfg.access.user_max_unique_ips_global_each,
|
global_each: cfg.access.user_max_unique_ips_global_each,
|
||||||
mode: user_max_unique_ips_mode_label(cfg.access.user_max_unique_ips_mode),
|
mode: user_max_unique_ips_mode_label(cfg.access.user_max_unique_ips_mode),
|
||||||
window_secs: cfg.access.user_max_unique_ips_window_secs,
|
window_secs: cfg.access.user_max_unique_ips_window_secs,
|
||||||
},
|
},
|
||||||
|
user_tcp_policy: EffectiveUserTcpPolicyLimits {
|
||||||
|
global_each: cfg.access.user_max_tcp_conns_global_each,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+164
-37
@@ -46,7 +46,9 @@ pub(super) async fn create_user(
|
|||||||
None => random_user_secret(),
|
None => random_user_secret(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ad_tag) = body.user_ad_tag.as_ref() && !is_valid_ad_tag(ad_tag) {
|
if let Some(ad_tag) = body.user_ad_tag.as_ref()
|
||||||
|
&& !is_valid_ad_tag(ad_tag)
|
||||||
|
{
|
||||||
return Err(ApiFailure::bad_request(
|
return Err(ApiFailure::bad_request(
|
||||||
"user_ad_tag must be exactly 32 hex characters",
|
"user_ad_tag must be exactly 32 hex characters",
|
||||||
));
|
));
|
||||||
@@ -65,12 +67,18 @@ pub(super) async fn create_user(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.access.users.insert(body.username.clone(), secret.clone());
|
cfg.access
|
||||||
|
.users
|
||||||
|
.insert(body.username.clone(), secret.clone());
|
||||||
if let Some(ad_tag) = body.user_ad_tag {
|
if let Some(ad_tag) = body.user_ad_tag {
|
||||||
cfg.access.user_ad_tags.insert(body.username.clone(), ad_tag);
|
cfg.access
|
||||||
|
.user_ad_tags
|
||||||
|
.insert(body.username.clone(), ad_tag);
|
||||||
}
|
}
|
||||||
if let Some(limit) = body.max_tcp_conns {
|
if let Some(limit) = body.max_tcp_conns {
|
||||||
cfg.access.user_max_tcp_conns.insert(body.username.clone(), limit);
|
cfg.access
|
||||||
|
.user_max_tcp_conns
|
||||||
|
.insert(body.username.clone(), limit);
|
||||||
}
|
}
|
||||||
if let Some(expiration) = expiration {
|
if let Some(expiration) = expiration {
|
||||||
cfg.access
|
cfg.access
|
||||||
@@ -78,7 +86,9 @@ pub(super) async fn create_user(
|
|||||||
.insert(body.username.clone(), expiration);
|
.insert(body.username.clone(), expiration);
|
||||||
}
|
}
|
||||||
if let Some(quota) = body.data_quota_bytes {
|
if let Some(quota) = body.data_quota_bytes {
|
||||||
cfg.access.user_data_quota.insert(body.username.clone(), quota);
|
cfg.access
|
||||||
|
.user_data_quota
|
||||||
|
.insert(body.username.clone(), quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
let updated_limit = body.max_unique_ips;
|
let updated_limit = body.max_unique_ips;
|
||||||
@@ -108,11 +118,15 @@ pub(super) async fn create_user(
|
|||||||
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
||||||
}
|
}
|
||||||
|
|
||||||
let revision = save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
let revision =
|
||||||
|
save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
|
|
||||||
if let Some(limit) = updated_limit {
|
if let Some(limit) = updated_limit {
|
||||||
shared.ip_tracker.set_user_limit(&body.username, limit).await;
|
shared
|
||||||
|
.ip_tracker
|
||||||
|
.set_user_limit(&body.username, limit)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
||||||
|
|
||||||
@@ -122,6 +136,7 @@ pub(super) async fn create_user(
|
|||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let user = users
|
let user = users
|
||||||
@@ -129,8 +144,16 @@ pub(super) async fn create_user(
|
|||||||
.find(|entry| entry.username == body.username)
|
.find(|entry| entry.username == body.username)
|
||||||
.unwrap_or(UserInfo {
|
.unwrap_or(UserInfo {
|
||||||
username: body.username.clone(),
|
username: body.username.clone(),
|
||||||
|
in_runtime: false,
|
||||||
user_ad_tag: None,
|
user_ad_tag: None,
|
||||||
max_tcp_conns: None,
|
max_tcp_conns: cfg
|
||||||
|
.access
|
||||||
|
.user_max_tcp_conns
|
||||||
|
.get(&body.username)
|
||||||
|
.copied()
|
||||||
|
.filter(|limit| *limit > 0)
|
||||||
|
.or((cfg.access.user_max_tcp_conns_global_each > 0)
|
||||||
|
.then_some(cfg.access.user_max_tcp_conns_global_each)),
|
||||||
expiration_rfc3339: None,
|
expiration_rfc3339: None,
|
||||||
data_quota_bytes: None,
|
data_quota_bytes: None,
|
||||||
max_unique_ips: updated_limit,
|
max_unique_ips: updated_limit,
|
||||||
@@ -140,12 +163,7 @@ pub(super) async fn create_user(
|
|||||||
recent_unique_ips: 0,
|
recent_unique_ips: 0,
|
||||||
recent_unique_ips_list: Vec::new(),
|
recent_unique_ips_list: Vec::new(),
|
||||||
total_octets: 0,
|
total_octets: 0,
|
||||||
links: build_user_links(
|
links: build_user_links(&cfg, &secret, detected_ip_v4, detected_ip_v6),
|
||||||
&cfg,
|
|
||||||
&secret,
|
|
||||||
detected_ip_v4,
|
|
||||||
detected_ip_v6,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok((CreateUserResponse { user, secret }, revision))
|
Ok((CreateUserResponse { user, secret }, revision))
|
||||||
@@ -157,12 +175,16 @@ pub(super) async fn patch_user(
|
|||||||
expected_revision: Option<String>,
|
expected_revision: Option<String>,
|
||||||
shared: &ApiShared,
|
shared: &ApiShared,
|
||||||
) -> Result<(UserInfo, String), ApiFailure> {
|
) -> Result<(UserInfo, String), ApiFailure> {
|
||||||
if let Some(secret) = body.secret.as_ref() && !is_valid_user_secret(secret) {
|
if let Some(secret) = body.secret.as_ref()
|
||||||
|
&& !is_valid_user_secret(secret)
|
||||||
|
{
|
||||||
return Err(ApiFailure::bad_request(
|
return Err(ApiFailure::bad_request(
|
||||||
"secret must be exactly 32 hex characters",
|
"secret must be exactly 32 hex characters",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(ad_tag) = body.user_ad_tag.as_ref() && !is_valid_ad_tag(ad_tag) {
|
if let Some(ad_tag) = body.user_ad_tag.as_ref()
|
||||||
|
&& !is_valid_ad_tag(ad_tag)
|
||||||
|
{
|
||||||
return Err(ApiFailure::bad_request(
|
return Err(ApiFailure::bad_request(
|
||||||
"user_ad_tag must be exactly 32 hex characters",
|
"user_ad_tag must be exactly 32 hex characters",
|
||||||
));
|
));
|
||||||
@@ -187,10 +209,14 @@ pub(super) async fn patch_user(
|
|||||||
cfg.access.user_ad_tags.insert(user.to_string(), ad_tag);
|
cfg.access.user_ad_tags.insert(user.to_string(), ad_tag);
|
||||||
}
|
}
|
||||||
if let Some(limit) = body.max_tcp_conns {
|
if let Some(limit) = body.max_tcp_conns {
|
||||||
cfg.access.user_max_tcp_conns.insert(user.to_string(), limit);
|
cfg.access
|
||||||
|
.user_max_tcp_conns
|
||||||
|
.insert(user.to_string(), limit);
|
||||||
}
|
}
|
||||||
if let Some(expiration) = expiration {
|
if let Some(expiration) = expiration {
|
||||||
cfg.access.user_expirations.insert(user.to_string(), expiration);
|
cfg.access
|
||||||
|
.user_expirations
|
||||||
|
.insert(user.to_string(), expiration);
|
||||||
}
|
}
|
||||||
if let Some(quota) = body.data_quota_bytes {
|
if let Some(quota) = body.data_quota_bytes {
|
||||||
cfg.access.user_data_quota.insert(user.to_string(), quota);
|
cfg.access.user_data_quota.insert(user.to_string(), quota);
|
||||||
@@ -198,7 +224,9 @@ pub(super) async fn patch_user(
|
|||||||
|
|
||||||
let mut updated_limit = None;
|
let mut updated_limit = None;
|
||||||
if let Some(limit) = body.max_unique_ips {
|
if let Some(limit) = body.max_unique_ips {
|
||||||
cfg.access.user_max_unique_ips.insert(user.to_string(), limit);
|
cfg.access
|
||||||
|
.user_max_unique_ips
|
||||||
|
.insert(user.to_string(), limit);
|
||||||
updated_limit = Some(limit);
|
updated_limit = Some(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +245,7 @@ pub(super) async fn patch_user(
|
|||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let user_info = users
|
let user_info = users
|
||||||
@@ -263,7 +292,8 @@ pub(super) async fn rotate_secret(
|
|||||||
AccessSection::UserDataQuota,
|
AccessSection::UserDataQuota,
|
||||||
AccessSection::UserMaxUniqueIps,
|
AccessSection::UserMaxUniqueIps,
|
||||||
];
|
];
|
||||||
let revision = save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
let revision =
|
||||||
|
save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
|
|
||||||
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
||||||
@@ -273,6 +303,7 @@ pub(super) async fn rotate_secret(
|
|||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let user_info = users
|
let user_info = users
|
||||||
@@ -330,7 +361,8 @@ pub(super) async fn delete_user(
|
|||||||
AccessSection::UserDataQuota,
|
AccessSection::UserDataQuota,
|
||||||
AccessSection::UserMaxUniqueIps,
|
AccessSection::UserMaxUniqueIps,
|
||||||
];
|
];
|
||||||
let revision = save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
let revision =
|
||||||
|
save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
shared.ip_tracker.remove_user_limit(user).await;
|
shared.ip_tracker.remove_user_limit(user).await;
|
||||||
shared.ip_tracker.clear_user_ips(user).await;
|
shared.ip_tracker.clear_user_ips(user).await;
|
||||||
@@ -344,6 +376,7 @@ pub(super) async fn users_from_config(
|
|||||||
ip_tracker: &UserIpTracker,
|
ip_tracker: &UserIpTracker,
|
||||||
startup_detected_ip_v4: Option<IpAddr>,
|
startup_detected_ip_v4: Option<IpAddr>,
|
||||||
startup_detected_ip_v6: Option<IpAddr>,
|
startup_detected_ip_v6: Option<IpAddr>,
|
||||||
|
runtime_cfg: Option<&ProxyConfig>,
|
||||||
) -> Vec<UserInfo> {
|
) -> Vec<UserInfo> {
|
||||||
let mut names = cfg.access.users.keys().cloned().collect::<Vec<_>>();
|
let mut names = cfg.access.users.keys().cloned().collect::<Vec<_>>();
|
||||||
names.sort();
|
names.sort();
|
||||||
@@ -365,12 +398,7 @@ pub(super) async fn users_from_config(
|
|||||||
.users
|
.users
|
||||||
.get(&username)
|
.get(&username)
|
||||||
.map(|secret| {
|
.map(|secret| {
|
||||||
build_user_links(
|
build_user_links(cfg, secret, startup_detected_ip_v4, startup_detected_ip_v6)
|
||||||
cfg,
|
|
||||||
secret,
|
|
||||||
startup_detected_ip_v4,
|
|
||||||
startup_detected_ip_v6,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.unwrap_or(UserLinks {
|
.unwrap_or(UserLinks {
|
||||||
classic: Vec::new(),
|
classic: Vec::new(),
|
||||||
@@ -378,8 +406,18 @@ pub(super) async fn users_from_config(
|
|||||||
tls: Vec::new(),
|
tls: Vec::new(),
|
||||||
});
|
});
|
||||||
users.push(UserInfo {
|
users.push(UserInfo {
|
||||||
|
in_runtime: runtime_cfg
|
||||||
|
.map(|runtime| runtime.access.users.contains_key(&username))
|
||||||
|
.unwrap_or(false),
|
||||||
user_ad_tag: cfg.access.user_ad_tags.get(&username).cloned(),
|
user_ad_tag: cfg.access.user_ad_tags.get(&username).cloned(),
|
||||||
max_tcp_conns: cfg.access.user_max_tcp_conns.get(&username).copied(),
|
max_tcp_conns: cfg
|
||||||
|
.access
|
||||||
|
.user_max_tcp_conns
|
||||||
|
.get(&username)
|
||||||
|
.copied()
|
||||||
|
.filter(|limit| *limit > 0)
|
||||||
|
.or((cfg.access.user_max_tcp_conns_global_each > 0)
|
||||||
|
.then_some(cfg.access.user_max_tcp_conns_global_each)),
|
||||||
expiration_rfc3339: cfg
|
expiration_rfc3339: cfg
|
||||||
.access
|
.access
|
||||||
.user_expirations
|
.user_expirations
|
||||||
@@ -392,10 +430,8 @@ pub(super) async fn users_from_config(
|
|||||||
.get(&username)
|
.get(&username)
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|limit| *limit > 0)
|
.filter(|limit| *limit > 0)
|
||||||
.or(
|
.or((cfg.access.user_max_unique_ips_global_each > 0)
|
||||||
(cfg.access.user_max_unique_ips_global_each > 0)
|
.then_some(cfg.access.user_max_unique_ips_global_each)),
|
||||||
.then_some(cfg.access.user_max_unique_ips_global_each),
|
|
||||||
),
|
|
||||||
current_connections: stats.get_user_curr_connects(&username),
|
current_connections: stats.get_user_curr_connects(&username),
|
||||||
active_unique_ips: active_ip_list.len(),
|
active_unique_ips: active_ip_list.len(),
|
||||||
active_unique_ips_list: active_ip_list,
|
active_unique_ips_list: active_ip_list,
|
||||||
@@ -481,11 +517,11 @@ fn resolve_link_hosts(
|
|||||||
push_unique_host(&mut hosts, host);
|
push_unique_host(&mut hosts, host);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(ip) = listener.announce_ip {
|
if let Some(ip) = listener.announce_ip
|
||||||
if !ip.is_unspecified() {
|
&& !ip.is_unspecified()
|
||||||
push_unique_host(&mut hosts, &ip.to_string());
|
{
|
||||||
continue;
|
push_unique_host(&mut hosts, &ip.to_string());
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
if listener.ip.is_unspecified() {
|
if listener.ip.is_unspecified() {
|
||||||
let detected_ip = if listener.ip.is_ipv4() {
|
let detected_ip = if listener.ip.is_ipv4() {
|
||||||
@@ -558,3 +594,94 @@ fn resolve_tls_domains(cfg: &ProxyConfig) -> Vec<&str> {
|
|||||||
}
|
}
|
||||||
domains
|
domains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ip_tracker::UserIpTracker;
|
||||||
|
use crate::stats::Stats;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn users_from_config_reports_effective_tcp_limit_with_global_fallback() {
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.access.users.insert(
|
||||||
|
"alice".to_string(),
|
||||||
|
"0123456789abcdef0123456789abcdef".to_string(),
|
||||||
|
);
|
||||||
|
cfg.access.user_max_tcp_conns_global_each = 7;
|
||||||
|
|
||||||
|
let stats = Stats::new();
|
||||||
|
let tracker = UserIpTracker::new();
|
||||||
|
|
||||||
|
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
||||||
|
let alice = users
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.username == "alice")
|
||||||
|
.expect("alice must be present");
|
||||||
|
assert!(!alice.in_runtime);
|
||||||
|
assert_eq!(alice.max_tcp_conns, Some(7));
|
||||||
|
|
||||||
|
cfg.access.user_max_tcp_conns.insert("alice".to_string(), 5);
|
||||||
|
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
||||||
|
let alice = users
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.username == "alice")
|
||||||
|
.expect("alice must be present");
|
||||||
|
assert!(!alice.in_runtime);
|
||||||
|
assert_eq!(alice.max_tcp_conns, Some(5));
|
||||||
|
|
||||||
|
cfg.access.user_max_tcp_conns.insert("alice".to_string(), 0);
|
||||||
|
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
||||||
|
let alice = users
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.username == "alice")
|
||||||
|
.expect("alice must be present");
|
||||||
|
assert!(!alice.in_runtime);
|
||||||
|
assert_eq!(alice.max_tcp_conns, Some(7));
|
||||||
|
|
||||||
|
cfg.access.user_max_tcp_conns_global_each = 0;
|
||||||
|
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
||||||
|
let alice = users
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.username == "alice")
|
||||||
|
.expect("alice must be present");
|
||||||
|
assert!(!alice.in_runtime);
|
||||||
|
assert_eq!(alice.max_tcp_conns, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn users_from_config_marks_runtime_membership_when_snapshot_is_provided() {
|
||||||
|
let mut disk_cfg = ProxyConfig::default();
|
||||||
|
disk_cfg.access.users.insert(
|
||||||
|
"alice".to_string(),
|
||||||
|
"0123456789abcdef0123456789abcdef".to_string(),
|
||||||
|
);
|
||||||
|
disk_cfg.access.users.insert(
|
||||||
|
"bob".to_string(),
|
||||||
|
"fedcba9876543210fedcba9876543210".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut runtime_cfg = ProxyConfig::default();
|
||||||
|
runtime_cfg.access.users.insert(
|
||||||
|
"alice".to_string(),
|
||||||
|
"0123456789abcdef0123456789abcdef".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let stats = Stats::new();
|
||||||
|
let tracker = UserIpTracker::new();
|
||||||
|
let users =
|
||||||
|
users_from_config(&disk_cfg, &stats, &tracker, None, None, Some(&runtime_cfg)).await;
|
||||||
|
|
||||||
|
let alice = users
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.username == "alice")
|
||||||
|
.expect("alice must be present");
|
||||||
|
let bob = users
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.username == "bob")
|
||||||
|
.expect("bob must be present");
|
||||||
|
|
||||||
|
assert!(alice.in_runtime);
|
||||||
|
assert!(!bob.in_runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+436
-77
@@ -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 std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use rand::Rng;
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use crate::daemon::{self, DEFAULT_PID_FILE, DaemonOptions};
|
||||||
|
|
||||||
|
/// 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 = std::env::current_exe()
|
let exe_path =
|
||||||
.unwrap_or_else(|_| PathBuf::from("/usr/local/bin/telemt"));
|
std::env::current_exe().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(())
|
||||||
@@ -183,7 +560,7 @@ fn generate_secret() -> String {
|
|||||||
|
|
||||||
fn generate_config(username: &str, secret: &str, port: u16, domain: &str) -> String {
|
fn generate_config(username: &str, secret: &str, port: u16, domain: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
r#"# Telemt MTProxy — auto-generated config
|
r#"# Telemt MTProxy — auto-generated config
|
||||||
# Re-run `telemt --init` to regenerate
|
# Re-run `telemt --init` to regenerate
|
||||||
|
|
||||||
show_link = ["{username}"]
|
show_link = ["{username}"]
|
||||||
@@ -198,8 +575,16 @@ desync_all_full = false
|
|||||||
update_every = 43200
|
update_every = 43200
|
||||||
hardswap = false
|
hardswap = false
|
||||||
me_pool_drain_ttl_secs = 90
|
me_pool_drain_ttl_secs = 90
|
||||||
|
me_instadrain = false
|
||||||
|
me_pool_drain_threshold = 32
|
||||||
|
me_pool_drain_soft_evict_grace_secs = 10
|
||||||
|
me_pool_drain_soft_evict_per_writer = 2
|
||||||
|
me_pool_drain_soft_evict_budget_per_core = 16
|
||||||
|
me_pool_drain_soft_evict_cooldown_ms = 1000
|
||||||
|
me_bind_stale_mode = "never"
|
||||||
me_pool_min_fresh_ratio = 0.8
|
me_pool_min_fresh_ratio = 0.8
|
||||||
me_reinit_drain_timeout_secs = 120
|
me_reinit_drain_timeout_secs = 90
|
||||||
|
tg_connect = 10
|
||||||
|
|
||||||
[network]
|
[network]
|
||||||
ipv4 = true
|
ipv4 = true
|
||||||
@@ -225,8 +610,8 @@ ip = "0.0.0.0"
|
|||||||
ip = "::"
|
ip = "::"
|
||||||
|
|
||||||
[timeouts]
|
[timeouts]
|
||||||
client_handshake = 15
|
client_first_byte_idle_secs = 300
|
||||||
tg_connect = 10
|
client_handshake = 60
|
||||||
client_keepalive = 60
|
client_keepalive = 60
|
||||||
client_ack = 300
|
client_ack = 300
|
||||||
|
|
||||||
@@ -238,6 +623,7 @@ fake_cert_len = 2048
|
|||||||
tls_full_cert_ttl_secs = 90
|
tls_full_cert_ttl_secs = 90
|
||||||
|
|
||||||
[access]
|
[access]
|
||||||
|
user_max_tcp_conns_global_each = 0
|
||||||
replay_check_len = 65536
|
replay_check_len = 65536
|
||||||
replay_window_secs = 120
|
replay_window_secs = 120
|
||||||
ignore_time_skew = false
|
ignore_time_skew = false
|
||||||
@@ -257,35 +643,6 @@ weight = 10
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_systemd_unit(exe_path: &Path, config_path: &Path) -> String {
|
|
||||||
format!(
|
|
||||||
r#"[Unit]
|
|
||||||
Description=Telemt MTProxy
|
|
||||||
Documentation=https://github.com/nicepkg/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) => {
|
||||||
@@ -305,8 +662,10 @@ fn print_links(username: &str, secret: &str, port: u16, domain: &str) {
|
|||||||
|
|
||||||
println!("=== Proxy Links ===");
|
println!("=== Proxy Links ===");
|
||||||
println!("[{}]", username);
|
println!("[{}]", username);
|
||||||
println!(" EE-TLS: tg://proxy?server=YOUR_SERVER_IP&port={}&secret=ee{}{}",
|
println!(
|
||||||
port, secret, domain_hex);
|
" EE-TLS: tg://proxy?server=YOUR_SERVER_IP&port={}&secret=ee{}{}",
|
||||||
|
port, secret, domain_hex
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
println!("Replace YOUR_SERVER_IP with your server's public IP.");
|
println!("Replace YOUR_SERVER_IP with your server's public IP.");
|
||||||
println!("The proxy will auto-detect and display the correct link on startup.");
|
println!("The proxy will auto-detect and display the correct link on startup.");
|
||||||
|
|||||||
+207
-17
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// Helper defaults kept private to the config module.
|
// Helper defaults kept private to the config module.
|
||||||
const DEFAULT_NETWORK_IPV6: Option<bool> = Some(false);
|
const DEFAULT_NETWORK_IPV6: Option<bool> = Some(false);
|
||||||
@@ -27,8 +27,10 @@ const DEFAULT_ME_C2ME_CHANNEL_CAPACITY: usize = 1024;
|
|||||||
const DEFAULT_ME_READER_ROUTE_DATA_WAIT_MS: u64 = 2;
|
const DEFAULT_ME_READER_ROUTE_DATA_WAIT_MS: u64 = 2;
|
||||||
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_FRAMES: usize = 32;
|
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 = 1500;
|
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_DELAY_US: u64 = 500;
|
||||||
const DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE: bool = false;
|
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;
|
||||||
@@ -36,7 +38,20 @@ const DEFAULT_ME_HEALTH_INTERVAL_MS_UNHEALTHY: u64 = 1000;
|
|||||||
const DEFAULT_ME_HEALTH_INTERVAL_MS_HEALTHY: u64 = 3000;
|
const DEFAULT_ME_HEALTH_INTERVAL_MS_HEALTHY: u64 = 3000;
|
||||||
const DEFAULT_ME_ADMISSION_POLL_MS: u64 = 1000;
|
const DEFAULT_ME_ADMISSION_POLL_MS: u64 = 1000;
|
||||||
const DEFAULT_ME_WARN_RATE_LIMIT_MS: u64 = 5000;
|
const DEFAULT_ME_WARN_RATE_LIMIT_MS: u64 = 5000;
|
||||||
|
const DEFAULT_ME_ROUTE_HYBRID_MAX_WAIT_MS: u64 = 3000;
|
||||||
|
const DEFAULT_ME_ROUTE_BLOCKING_SEND_TIMEOUT_MS: u64 = 250;
|
||||||
|
const DEFAULT_ME_C2ME_SEND_TIMEOUT_MS: u64 = 4000;
|
||||||
|
const DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_ENABLED: bool = true;
|
||||||
|
const DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_GRACE_SECS: u64 = 10;
|
||||||
|
const DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_PER_WRITER: u8 = 2;
|
||||||
|
const DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_BUDGET_PER_CORE: u16 = 16;
|
||||||
|
const DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_COOLDOWN_MS: u64 = 1000;
|
||||||
const DEFAULT_USER_MAX_UNIQUE_IPS_WINDOW_SECS: u64 = 30;
|
const DEFAULT_USER_MAX_UNIQUE_IPS_WINDOW_SECS: u64 = 30;
|
||||||
|
const DEFAULT_ACCEPT_PERMIT_TIMEOUT_MS: u64 = 250;
|
||||||
|
const DEFAULT_CONNTRACK_CONTROL_ENABLED: bool = true;
|
||||||
|
const DEFAULT_CONNTRACK_PRESSURE_HIGH_WATERMARK_PCT: u8 = 85;
|
||||||
|
const DEFAULT_CONNTRACK_PRESSURE_LOW_WATERMARK_PCT: u8 = 70;
|
||||||
|
const DEFAULT_CONNTRACK_DELETE_BUDGET_PER_SEC: u64 = 4096;
|
||||||
const DEFAULT_UPSTREAM_CONNECT_RETRY_ATTEMPTS: u32 = 2;
|
const DEFAULT_UPSTREAM_CONNECT_RETRY_ATTEMPTS: u32 = 2;
|
||||||
const DEFAULT_UPSTREAM_UNHEALTHY_FAIL_THRESHOLD: u32 = 5;
|
const DEFAULT_UPSTREAM_UNHEALTHY_FAIL_THRESHOLD: u32 = 5;
|
||||||
const DEFAULT_UPSTREAM_CONNECT_BUDGET_MS: u64 = 3000;
|
const DEFAULT_UPSTREAM_CONNECT_BUDGET_MS: u64 = 3000;
|
||||||
@@ -56,6 +71,26 @@ pub(crate) fn default_tls_domain() -> String {
|
|||||||
"petrovich.ru".to_string()
|
"petrovich.ru".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_tls_fetch_scope() -> String {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_tls_fetch_attempt_timeout_ms() -> u64 {
|
||||||
|
5_000
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_tls_fetch_total_budget_ms() -> u64 {
|
||||||
|
15_000
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_tls_fetch_strict_route() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_tls_fetch_profile_cache_ttl_secs() -> u64 {
|
||||||
|
600
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_mask_port() -> u16 {
|
pub(crate) fn default_mask_port() -> u16 {
|
||||||
443
|
443
|
||||||
}
|
}
|
||||||
@@ -65,7 +100,7 @@ pub(crate) fn default_fake_cert_len() -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_tls_front_dir() -> String {
|
pub(crate) fn default_tls_front_dir() -> String {
|
||||||
"tlsfront".to_string()
|
"/etc/telemt/tlsfront".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_replay_check_len() -> usize {
|
pub(crate) fn default_replay_check_len() -> usize {
|
||||||
@@ -79,6 +114,26 @@ pub(crate) fn default_replay_window_secs() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_handshake_timeout() -> u64 {
|
pub(crate) fn default_handshake_timeout() -> u64 {
|
||||||
|
60
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_client_first_byte_idle_secs() -> u64 {
|
||||||
|
300
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_relay_idle_policy_v2_enabled() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_relay_client_idle_soft_secs() -> u64 {
|
||||||
|
120
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_relay_client_idle_hard_secs() -> u64 {
|
||||||
|
360
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_relay_idle_grace_after_downstream_activity_secs() -> u64 {
|
||||||
30
|
30
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +142,11 @@ pub(crate) fn default_connect_timeout() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_keepalive() -> u64 {
|
pub(crate) fn default_keepalive() -> u64 {
|
||||||
60
|
15
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_ack_timeout() -> u64 {
|
pub(crate) fn default_ack_timeout() -> u64 {
|
||||||
300
|
90
|
||||||
}
|
}
|
||||||
pub(crate) fn default_me_one_retry() -> u8 {
|
pub(crate) fn default_me_one_retry() -> u8 {
|
||||||
12
|
12
|
||||||
@@ -114,10 +169,7 @@ pub(crate) fn default_weight() -> u16 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_metrics_whitelist() -> Vec<IpNetwork> {
|
pub(crate) fn default_metrics_whitelist() -> Vec<IpNetwork> {
|
||||||
vec![
|
vec!["127.0.0.1/32".parse().unwrap(), "::1/128".parse().unwrap()]
|
||||||
"127.0.0.1/32".parse().unwrap(),
|
|
||||||
"::1/128".parse().unwrap(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_api_listen() -> String {
|
pub(crate) fn default_api_listen() -> String {
|
||||||
@@ -140,19 +192,55 @@ pub(crate) fn default_api_minimal_runtime_cache_ttl_ms() -> u64 {
|
|||||||
1000
|
1000
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_api_runtime_edge_enabled() -> bool { false }
|
pub(crate) fn default_api_runtime_edge_enabled() -> bool {
|
||||||
pub(crate) fn default_api_runtime_edge_cache_ttl_ms() -> u64 { 1000 }
|
false
|
||||||
pub(crate) fn default_api_runtime_edge_top_n() -> usize { 10 }
|
}
|
||||||
pub(crate) fn default_api_runtime_edge_events_capacity() -> usize { 256 }
|
pub(crate) fn default_api_runtime_edge_cache_ttl_ms() -> u64 {
|
||||||
|
1000
|
||||||
|
}
|
||||||
|
pub(crate) fn default_api_runtime_edge_top_n() -> usize {
|
||||||
|
10
|
||||||
|
}
|
||||||
|
pub(crate) fn default_api_runtime_edge_events_capacity() -> usize {
|
||||||
|
256
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_proxy_protocol_header_timeout_ms() -> u64 {
|
pub(crate) fn default_proxy_protocol_header_timeout_ms() -> u64 {
|
||||||
500
|
500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_proxy_protocol_trusted_cidrs() -> Vec<IpNetwork> {
|
||||||
|
vec!["0.0.0.0/0".parse().unwrap(), "::/0".parse().unwrap()]
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_server_max_connections() -> u32 {
|
pub(crate) fn default_server_max_connections() -> u32 {
|
||||||
10_000
|
10_000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_listen_backlog() -> u32 {
|
||||||
|
1024
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_accept_permit_timeout_ms() -> u64 {
|
||||||
|
DEFAULT_ACCEPT_PERMIT_TIMEOUT_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_conntrack_control_enabled() -> bool {
|
||||||
|
DEFAULT_CONNTRACK_CONTROL_ENABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_conntrack_pressure_high_watermark_pct() -> u8 {
|
||||||
|
DEFAULT_CONNTRACK_PRESSURE_HIGH_WATERMARK_PCT
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_conntrack_pressure_low_watermark_pct() -> u8 {
|
||||||
|
DEFAULT_CONNTRACK_PRESSURE_LOW_WATERMARK_PCT
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_conntrack_delete_budget_per_sec() -> u64 {
|
||||||
|
DEFAULT_CONNTRACK_DELETE_BUDGET_PER_SEC
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_prefer_4() -> u8 {
|
pub(crate) fn default_prefer_4() -> u8 {
|
||||||
4
|
4
|
||||||
}
|
}
|
||||||
@@ -213,6 +301,10 @@ pub(crate) fn default_me2dc_fallback() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me2dc_fast() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_keepalive_interval() -> u64 {
|
pub(crate) fn default_keepalive_interval() -> u64 {
|
||||||
8
|
8
|
||||||
}
|
}
|
||||||
@@ -349,6 +441,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
|
||||||
}
|
}
|
||||||
@@ -377,6 +477,18 @@ pub(crate) fn default_me_warn_rate_limit_ms() -> u64 {
|
|||||||
DEFAULT_ME_WARN_RATE_LIMIT_MS
|
DEFAULT_ME_WARN_RATE_LIMIT_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_route_hybrid_max_wait_ms() -> u64 {
|
||||||
|
DEFAULT_ME_ROUTE_HYBRID_MAX_WAIT_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_route_blocking_send_timeout_ms() -> u64 {
|
||||||
|
DEFAULT_ME_ROUTE_BLOCKING_SEND_TIMEOUT_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_c2me_send_timeout_ms() -> u64 {
|
||||||
|
DEFAULT_ME_C2ME_SEND_TIMEOUT_MS
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_upstream_connect_retry_attempts() -> u32 {
|
pub(crate) fn default_upstream_connect_retry_attempts() -> u32 {
|
||||||
DEFAULT_UPSTREAM_CONNECT_RETRY_ATTEMPTS
|
DEFAULT_UPSTREAM_CONNECT_RETRY_ATTEMPTS
|
||||||
}
|
}
|
||||||
@@ -446,7 +558,7 @@ pub(crate) fn default_beobachten_flush_secs() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_beobachten_file() -> String {
|
pub(crate) fn default_beobachten_file() -> String {
|
||||||
"cache/beobachten.txt".to_string()
|
"/etc/telemt/beobachten.txt".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_tls_new_session_tickets() -> u8 {
|
pub(crate) fn default_tls_new_session_tickets() -> u8 {
|
||||||
@@ -469,6 +581,56 @@ pub(crate) fn default_alpn_enforce() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_shape_hardening() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_shape_hardening_aggressive_mode() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_shape_bucket_floor_bytes() -> usize {
|
||||||
|
512
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_shape_bucket_cap_bytes() -> usize {
|
||||||
|
4096
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_shape_above_cap_blur() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_shape_above_cap_blur_max_bytes() -> usize {
|
||||||
|
512
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
pub(crate) fn default_mask_relay_max_bytes() -> usize {
|
||||||
|
5 * 1024 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn default_mask_relay_max_bytes() -> usize {
|
||||||
|
32 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_classifier_prefetch_timeout_ms() -> u64 {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_timing_normalization_enabled() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_timing_normalization_floor_ms() -> u64 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_mask_timing_normalization_ceiling_ms() -> u64 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_stun_servers() -> Vec<String> {
|
pub(crate) fn default_stun_servers() -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
"stun.l.google.com:5349".to_string(),
|
"stun.l.google.com:5349".to_string(),
|
||||||
@@ -583,15 +745,39 @@ pub(crate) fn default_proxy_secret_len_max() -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_me_reinit_drain_timeout_secs() -> u64 {
|
pub(crate) fn default_me_reinit_drain_timeout_secs() -> u64 {
|
||||||
120
|
90
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_me_pool_drain_ttl_secs() -> u64 {
|
pub(crate) fn default_me_pool_drain_ttl_secs() -> u64 {
|
||||||
90
|
90
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_instadrain() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_me_pool_drain_threshold() -> u64 {
|
pub(crate) fn default_me_pool_drain_threshold() -> u64 {
|
||||||
128
|
32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_pool_drain_soft_evict_enabled() -> bool {
|
||||||
|
DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_ENABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_pool_drain_soft_evict_grace_secs() -> u64 {
|
||||||
|
DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_GRACE_SECS
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_pool_drain_soft_evict_per_writer() -> u8 {
|
||||||
|
DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_PER_WRITER
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_pool_drain_soft_evict_budget_per_core() -> u16 {
|
||||||
|
DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_BUDGET_PER_CORE
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_me_pool_drain_soft_evict_cooldown_ms() -> u64 {
|
||||||
|
DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_COOLDOWN_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_me_bind_stale_ttl_secs() -> u64 {
|
pub(crate) fn default_me_bind_stale_ttl_secs() -> u64 {
|
||||||
@@ -645,6 +831,10 @@ pub(crate) fn default_user_max_unique_ips_window_secs() -> u64 {
|
|||||||
DEFAULT_USER_MAX_UNIQUE_IPS_WINDOW_SECS
|
DEFAULT_USER_MAX_UNIQUE_IPS_WINDOW_SECS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_user_max_tcp_conns_global_each() -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_user_max_unique_ips_global_each() -> usize {
|
pub(crate) fn default_user_max_unique_ips_global_each() -> usize {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
+219
-101
@@ -31,11 +31,10 @@ use notify::{EventKind, RecursiveMode, Watcher, recommended_watcher};
|
|||||||
use tokio::sync::{mpsc, watch};
|
use tokio::sync::{mpsc, watch};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::config::{
|
|
||||||
LogLevel, MeBindStaleMode, MeFloorMode, MeSocksKdfPolicy, MeTelemetryLevel,
|
|
||||||
MeWriterPickMode,
|
|
||||||
};
|
|
||||||
use super::load::{LoadedConfig, ProxyConfig};
|
use super::load::{LoadedConfig, ProxyConfig};
|
||||||
|
use crate::config::{
|
||||||
|
LogLevel, MeBindStaleMode, MeFloorMode, MeSocksKdfPolicy, MeTelemetryLevel, MeWriterPickMode,
|
||||||
|
};
|
||||||
|
|
||||||
const HOT_RELOAD_DEBOUNCE: Duration = Duration::from_millis(50);
|
const HOT_RELOAD_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
@@ -44,16 +43,17 @@ const HOT_RELOAD_DEBOUNCE: Duration = Duration::from_millis(50);
|
|||||||
/// Fields that are safe to swap without restarting listeners.
|
/// Fields that are safe to swap without restarting listeners.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct HotFields {
|
pub struct HotFields {
|
||||||
pub log_level: LogLevel,
|
pub log_level: LogLevel,
|
||||||
pub ad_tag: Option<String>,
|
pub ad_tag: Option<String>,
|
||||||
pub dns_overrides: Vec<String>,
|
pub dns_overrides: Vec<String>,
|
||||||
pub desync_all_full: bool,
|
pub desync_all_full: bool,
|
||||||
pub update_every_secs: u64,
|
pub update_every_secs: u64,
|
||||||
pub me_reinit_every_secs: u64,
|
pub me_reinit_every_secs: u64,
|
||||||
pub me_reinit_singleflight: bool,
|
pub me_reinit_singleflight: bool,
|
||||||
pub me_reinit_coalesce_window_ms: u64,
|
pub me_reinit_coalesce_window_ms: u64,
|
||||||
pub hardswap: bool,
|
pub hardswap: bool,
|
||||||
pub me_pool_drain_ttl_secs: u64,
|
pub me_pool_drain_ttl_secs: u64,
|
||||||
|
pub me_instadrain: bool,
|
||||||
pub me_pool_drain_threshold: u64,
|
pub me_pool_drain_threshold: u64,
|
||||||
pub me_pool_min_fresh_ratio: f32,
|
pub me_pool_min_fresh_ratio: f32,
|
||||||
pub me_reinit_drain_timeout_secs: u64,
|
pub me_reinit_drain_timeout_secs: u64,
|
||||||
@@ -106,18 +106,21 @@ 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,
|
||||||
pub me_health_interval_ms_healthy: u64,
|
pub me_health_interval_ms_healthy: u64,
|
||||||
pub me_admission_poll_ms: u64,
|
pub me_admission_poll_ms: u64,
|
||||||
pub me_warn_rate_limit_ms: u64,
|
pub me_warn_rate_limit_ms: u64,
|
||||||
pub users: std::collections::HashMap<String, String>,
|
pub users: std::collections::HashMap<String, String>,
|
||||||
pub user_ad_tags: std::collections::HashMap<String, String>,
|
pub user_ad_tags: std::collections::HashMap<String, String>,
|
||||||
pub user_max_tcp_conns: std::collections::HashMap<String, usize>,
|
pub user_max_tcp_conns: std::collections::HashMap<String, usize>,
|
||||||
pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
|
pub user_max_tcp_conns_global_each: usize,
|
||||||
pub user_data_quota: std::collections::HashMap<String, u64>,
|
pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
|
||||||
pub user_max_unique_ips: std::collections::HashMap<String, usize>,
|
pub user_data_quota: std::collections::HashMap<String, u64>,
|
||||||
|
pub user_max_unique_ips: std::collections::HashMap<String, usize>,
|
||||||
pub user_max_unique_ips_global_each: usize,
|
pub user_max_unique_ips_global_each: usize,
|
||||||
pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode,
|
pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode,
|
||||||
pub user_max_unique_ips_window_secs: u64,
|
pub user_max_unique_ips_window_secs: u64,
|
||||||
@@ -126,16 +129,17 @@ pub struct HotFields {
|
|||||||
impl HotFields {
|
impl HotFields {
|
||||||
pub fn from_config(cfg: &ProxyConfig) -> Self {
|
pub fn from_config(cfg: &ProxyConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
log_level: cfg.general.log_level.clone(),
|
log_level: cfg.general.log_level.clone(),
|
||||||
ad_tag: cfg.general.ad_tag.clone(),
|
ad_tag: cfg.general.ad_tag.clone(),
|
||||||
dns_overrides: cfg.network.dns_overrides.clone(),
|
dns_overrides: cfg.network.dns_overrides.clone(),
|
||||||
desync_all_full: cfg.general.desync_all_full,
|
desync_all_full: cfg.general.desync_all_full,
|
||||||
update_every_secs: cfg.general.effective_update_every_secs(),
|
update_every_secs: cfg.general.effective_update_every_secs(),
|
||||||
me_reinit_every_secs: cfg.general.me_reinit_every_secs,
|
me_reinit_every_secs: cfg.general.me_reinit_every_secs,
|
||||||
me_reinit_singleflight: cfg.general.me_reinit_singleflight,
|
me_reinit_singleflight: cfg.general.me_reinit_singleflight,
|
||||||
me_reinit_coalesce_window_ms: cfg.general.me_reinit_coalesce_window_ms,
|
me_reinit_coalesce_window_ms: cfg.general.me_reinit_coalesce_window_ms,
|
||||||
hardswap: cfg.general.hardswap,
|
hardswap: cfg.general.hardswap,
|
||||||
me_pool_drain_ttl_secs: cfg.general.me_pool_drain_ttl_secs,
|
me_pool_drain_ttl_secs: cfg.general.me_pool_drain_ttl_secs,
|
||||||
|
me_instadrain: cfg.general.me_instadrain,
|
||||||
me_pool_drain_threshold: cfg.general.me_pool_drain_threshold,
|
me_pool_drain_threshold: cfg.general.me_pool_drain_threshold,
|
||||||
me_pool_min_fresh_ratio: cfg.general.me_pool_min_fresh_ratio,
|
me_pool_min_fresh_ratio: cfg.general.me_pool_min_fresh_ratio,
|
||||||
me_reinit_drain_timeout_secs: cfg.general.me_reinit_drain_timeout_secs,
|
me_reinit_drain_timeout_secs: cfg.general.me_reinit_drain_timeout_secs,
|
||||||
@@ -187,15 +191,11 @@ impl HotFields {
|
|||||||
me_adaptive_floor_min_writers_multi_endpoint: cfg
|
me_adaptive_floor_min_writers_multi_endpoint: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_min_writers_multi_endpoint,
|
.me_adaptive_floor_min_writers_multi_endpoint,
|
||||||
me_adaptive_floor_recover_grace_secs: cfg
|
me_adaptive_floor_recover_grace_secs: cfg.general.me_adaptive_floor_recover_grace_secs,
|
||||||
.general
|
|
||||||
.me_adaptive_floor_recover_grace_secs,
|
|
||||||
me_adaptive_floor_writers_per_core_total: cfg
|
me_adaptive_floor_writers_per_core_total: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_writers_per_core_total,
|
.me_adaptive_floor_writers_per_core_total,
|
||||||
me_adaptive_floor_cpu_cores_override: cfg
|
me_adaptive_floor_cpu_cores_override: cfg.general.me_adaptive_floor_cpu_cores_override,
|
||||||
.general
|
|
||||||
.me_adaptive_floor_cpu_cores_override,
|
|
||||||
me_adaptive_floor_max_extra_writers_single_per_core: cfg
|
me_adaptive_floor_max_extra_writers_single_per_core: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_max_extra_writers_single_per_core,
|
.me_adaptive_floor_max_extra_writers_single_per_core,
|
||||||
@@ -214,26 +214,37 @@ impl HotFields {
|
|||||||
me_adaptive_floor_max_warm_writers_global: cfg
|
me_adaptive_floor_max_warm_writers_global: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_max_warm_writers_global,
|
.me_adaptive_floor_max_warm_writers_global,
|
||||||
me_route_backpressure_base_timeout_ms: cfg.general.me_route_backpressure_base_timeout_ms,
|
me_route_backpressure_base_timeout_ms: cfg
|
||||||
me_route_backpressure_high_timeout_ms: cfg.general.me_route_backpressure_high_timeout_ms,
|
.general
|
||||||
me_route_backpressure_high_watermark_pct: cfg.general.me_route_backpressure_high_watermark_pct,
|
.me_route_backpressure_base_timeout_ms,
|
||||||
|
me_route_backpressure_high_timeout_ms: cfg
|
||||||
|
.general
|
||||||
|
.me_route_backpressure_high_timeout_ms,
|
||||||
|
me_route_backpressure_high_watermark_pct: cfg
|
||||||
|
.general
|
||||||
|
.me_route_backpressure_high_watermark_pct,
|
||||||
me_reader_route_data_wait_ms: cfg.general.me_reader_route_data_wait_ms,
|
me_reader_route_data_wait_ms: cfg.general.me_reader_route_data_wait_ms,
|
||||||
me_d2c_flush_batch_max_frames: cfg.general.me_d2c_flush_batch_max_frames,
|
me_d2c_flush_batch_max_frames: cfg.general.me_d2c_flush_batch_max_frames,
|
||||||
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,
|
||||||
me_health_interval_ms_healthy: cfg.general.me_health_interval_ms_healthy,
|
me_health_interval_ms_healthy: cfg.general.me_health_interval_ms_healthy,
|
||||||
me_admission_poll_ms: cfg.general.me_admission_poll_ms,
|
me_admission_poll_ms: cfg.general.me_admission_poll_ms,
|
||||||
me_warn_rate_limit_ms: cfg.general.me_warn_rate_limit_ms,
|
me_warn_rate_limit_ms: cfg.general.me_warn_rate_limit_ms,
|
||||||
users: cfg.access.users.clone(),
|
users: cfg.access.users.clone(),
|
||||||
user_ad_tags: cfg.access.user_ad_tags.clone(),
|
user_ad_tags: cfg.access.user_ad_tags.clone(),
|
||||||
user_max_tcp_conns: cfg.access.user_max_tcp_conns.clone(),
|
user_max_tcp_conns: cfg.access.user_max_tcp_conns.clone(),
|
||||||
user_expirations: cfg.access.user_expirations.clone(),
|
user_max_tcp_conns_global_each: cfg.access.user_max_tcp_conns_global_each,
|
||||||
user_data_quota: cfg.access.user_data_quota.clone(),
|
user_expirations: cfg.access.user_expirations.clone(),
|
||||||
user_max_unique_ips: cfg.access.user_max_unique_ips.clone(),
|
user_data_quota: cfg.access.user_data_quota.clone(),
|
||||||
|
user_max_unique_ips: cfg.access.user_max_unique_ips.clone(),
|
||||||
user_max_unique_ips_global_each: cfg.access.user_max_unique_ips_global_each,
|
user_max_unique_ips_global_each: cfg.access.user_max_unique_ips_global_each,
|
||||||
user_max_unique_ips_mode: cfg.access.user_max_unique_ips_mode,
|
user_max_unique_ips_mode: cfg.access.user_max_unique_ips_mode,
|
||||||
user_max_unique_ips_window_secs: cfg.access.user_max_unique_ips_window_secs,
|
user_max_unique_ips_window_secs: cfg.access.user_max_unique_ips_window_secs,
|
||||||
@@ -332,7 +343,9 @@ struct ReloadState {
|
|||||||
|
|
||||||
impl ReloadState {
|
impl ReloadState {
|
||||||
fn new(applied_snapshot_hash: Option<u64>) -> Self {
|
fn new(applied_snapshot_hash: Option<u64>) -> Self {
|
||||||
Self { applied_snapshot_hash }
|
Self {
|
||||||
|
applied_snapshot_hash,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_applied(&self, hash: u64) -> bool {
|
fn is_applied(&self, hash: u64) -> bool {
|
||||||
@@ -431,6 +444,7 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
|||||||
cfg.general.me_reinit_coalesce_window_ms = new.general.me_reinit_coalesce_window_ms;
|
cfg.general.me_reinit_coalesce_window_ms = new.general.me_reinit_coalesce_window_ms;
|
||||||
cfg.general.hardswap = new.general.hardswap;
|
cfg.general.hardswap = new.general.hardswap;
|
||||||
cfg.general.me_pool_drain_ttl_secs = new.general.me_pool_drain_ttl_secs;
|
cfg.general.me_pool_drain_ttl_secs = new.general.me_pool_drain_ttl_secs;
|
||||||
|
cfg.general.me_instadrain = new.general.me_instadrain;
|
||||||
cfg.general.me_pool_drain_threshold = new.general.me_pool_drain_threshold;
|
cfg.general.me_pool_drain_threshold = new.general.me_pool_drain_threshold;
|
||||||
cfg.general.me_pool_min_fresh_ratio = new.general.me_pool_min_fresh_ratio;
|
cfg.general.me_pool_min_fresh_ratio = new.general.me_pool_min_fresh_ratio;
|
||||||
cfg.general.me_reinit_drain_timeout_secs = new.general.me_reinit_drain_timeout_secs;
|
cfg.general.me_reinit_drain_timeout_secs = new.general.me_reinit_drain_timeout_secs;
|
||||||
@@ -478,10 +492,14 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
|||||||
new.general.me_adaptive_floor_writers_per_core_total;
|
new.general.me_adaptive_floor_writers_per_core_total;
|
||||||
cfg.general.me_adaptive_floor_cpu_cores_override =
|
cfg.general.me_adaptive_floor_cpu_cores_override =
|
||||||
new.general.me_adaptive_floor_cpu_cores_override;
|
new.general.me_adaptive_floor_cpu_cores_override;
|
||||||
cfg.general.me_adaptive_floor_max_extra_writers_single_per_core =
|
cfg.general
|
||||||
new.general.me_adaptive_floor_max_extra_writers_single_per_core;
|
.me_adaptive_floor_max_extra_writers_single_per_core = new
|
||||||
cfg.general.me_adaptive_floor_max_extra_writers_multi_per_core =
|
.general
|
||||||
new.general.me_adaptive_floor_max_extra_writers_multi_per_core;
|
.me_adaptive_floor_max_extra_writers_single_per_core;
|
||||||
|
cfg.general
|
||||||
|
.me_adaptive_floor_max_extra_writers_multi_per_core = new
|
||||||
|
.general
|
||||||
|
.me_adaptive_floor_max_extra_writers_multi_per_core;
|
||||||
cfg.general.me_adaptive_floor_max_active_writers_per_core =
|
cfg.general.me_adaptive_floor_max_active_writers_per_core =
|
||||||
new.general.me_adaptive_floor_max_active_writers_per_core;
|
new.general.me_adaptive_floor_max_active_writers_per_core;
|
||||||
cfg.general.me_adaptive_floor_max_warm_writers_per_core =
|
cfg.general.me_adaptive_floor_max_warm_writers_per_core =
|
||||||
@@ -501,6 +519,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;
|
||||||
@@ -511,6 +532,7 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
|||||||
cfg.access.users = new.access.users.clone();
|
cfg.access.users = new.access.users.clone();
|
||||||
cfg.access.user_ad_tags = new.access.user_ad_tags.clone();
|
cfg.access.user_ad_tags = new.access.user_ad_tags.clone();
|
||||||
cfg.access.user_max_tcp_conns = new.access.user_max_tcp_conns.clone();
|
cfg.access.user_max_tcp_conns = new.access.user_max_tcp_conns.clone();
|
||||||
|
cfg.access.user_max_tcp_conns_global_each = new.access.user_max_tcp_conns_global_each;
|
||||||
cfg.access.user_expirations = new.access.user_expirations.clone();
|
cfg.access.user_expirations = new.access.user_expirations.clone();
|
||||||
cfg.access.user_data_quota = new.access.user_data_quota.clone();
|
cfg.access.user_data_quota = new.access.user_data_quota.clone();
|
||||||
cfg.access.user_max_unique_ips = new.access.user_max_unique_ips.clone();
|
cfg.access.user_max_unique_ips = new.access.user_max_unique_ips.clone();
|
||||||
@@ -540,8 +562,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
|| old.server.api.minimal_runtime_cache_ttl_ms
|
|| old.server.api.minimal_runtime_cache_ttl_ms
|
||||||
!= new.server.api.minimal_runtime_cache_ttl_ms
|
!= new.server.api.minimal_runtime_cache_ttl_ms
|
||||||
|| old.server.api.runtime_edge_enabled != new.server.api.runtime_edge_enabled
|
|| old.server.api.runtime_edge_enabled != new.server.api.runtime_edge_enabled
|
||||||
|| old.server.api.runtime_edge_cache_ttl_ms
|
|| old.server.api.runtime_edge_cache_ttl_ms != new.server.api.runtime_edge_cache_ttl_ms
|
||||||
!= new.server.api.runtime_edge_cache_ttl_ms
|
|
||||||
|| old.server.api.runtime_edge_top_n != new.server.api.runtime_edge_top_n
|
|| old.server.api.runtime_edge_top_n != new.server.api.runtime_edge_top_n
|
||||||
|| old.server.api.runtime_edge_events_capacity
|
|| old.server.api.runtime_edge_events_capacity
|
||||||
!= new.server.api.runtime_edge_events_capacity
|
!= new.server.api.runtime_edge_events_capacity
|
||||||
@@ -552,6 +573,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
}
|
}
|
||||||
if old.server.proxy_protocol != new.server.proxy_protocol
|
if old.server.proxy_protocol != new.server.proxy_protocol
|
||||||
|| !listeners_equal(&old.server.listeners, &new.server.listeners)
|
|| !listeners_equal(&old.server.listeners, &new.server.listeners)
|
||||||
|
|| old.server.listen_backlog != new.server.listen_backlog
|
||||||
|| old.server.listen_addr_ipv4 != new.server.listen_addr_ipv4
|
|| old.server.listen_addr_ipv4 != new.server.listen_addr_ipv4
|
||||||
|| old.server.listen_addr_ipv6 != new.server.listen_addr_ipv6
|
|| old.server.listen_addr_ipv6 != new.server.listen_addr_ipv6
|
||||||
|| old.server.listen_tcp != new.server.listen_tcp
|
|| old.server.listen_tcp != new.server.listen_tcp
|
||||||
@@ -563,6 +585,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
}
|
}
|
||||||
if old.censorship.tls_domain != new.censorship.tls_domain
|
if old.censorship.tls_domain != new.censorship.tls_domain
|
||||||
|| old.censorship.tls_domains != new.censorship.tls_domains
|
|| old.censorship.tls_domains != new.censorship.tls_domains
|
||||||
|
|| old.censorship.tls_fetch_scope != new.censorship.tls_fetch_scope
|
||||||
|| old.censorship.mask != new.censorship.mask
|
|| old.censorship.mask != new.censorship.mask
|
||||||
|| old.censorship.mask_host != new.censorship.mask_host
|
|| old.censorship.mask_host != new.censorship.mask_host
|
||||||
|| old.censorship.mask_port != new.censorship.mask_port
|
|| old.censorship.mask_port != new.censorship.mask_port
|
||||||
@@ -576,6 +599,22 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
|| old.censorship.tls_full_cert_ttl_secs != new.censorship.tls_full_cert_ttl_secs
|
|| old.censorship.tls_full_cert_ttl_secs != new.censorship.tls_full_cert_ttl_secs
|
||||||
|| old.censorship.alpn_enforce != new.censorship.alpn_enforce
|
|| old.censorship.alpn_enforce != new.censorship.alpn_enforce
|
||||||
|| old.censorship.mask_proxy_protocol != new.censorship.mask_proxy_protocol
|
|| old.censorship.mask_proxy_protocol != new.censorship.mask_proxy_protocol
|
||||||
|
|| old.censorship.mask_shape_hardening != new.censorship.mask_shape_hardening
|
||||||
|
|| old.censorship.mask_shape_bucket_floor_bytes
|
||||||
|
!= new.censorship.mask_shape_bucket_floor_bytes
|
||||||
|
|| old.censorship.mask_shape_bucket_cap_bytes != new.censorship.mask_shape_bucket_cap_bytes
|
||||||
|
|| old.censorship.mask_shape_above_cap_blur != new.censorship.mask_shape_above_cap_blur
|
||||||
|
|| old.censorship.mask_shape_above_cap_blur_max_bytes
|
||||||
|
!= new.censorship.mask_shape_above_cap_blur_max_bytes
|
||||||
|
|| old.censorship.mask_relay_max_bytes != new.censorship.mask_relay_max_bytes
|
||||||
|
|| old.censorship.mask_classifier_prefetch_timeout_ms
|
||||||
|
!= new.censorship.mask_classifier_prefetch_timeout_ms
|
||||||
|
|| old.censorship.mask_timing_normalization_enabled
|
||||||
|
!= new.censorship.mask_timing_normalization_enabled
|
||||||
|
|| old.censorship.mask_timing_normalization_floor_ms
|
||||||
|
!= new.censorship.mask_timing_normalization_floor_ms
|
||||||
|
|| old.censorship.mask_timing_normalization_ceiling_ms
|
||||||
|
!= new.censorship.mask_timing_normalization_ceiling_ms
|
||||||
{
|
{
|
||||||
warned = true;
|
warned = true;
|
||||||
warn!("config reload: censorship settings changed; restart required");
|
warn!("config reload: censorship settings changed; restart required");
|
||||||
@@ -616,6 +655,9 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
}
|
}
|
||||||
if old.general.me_route_no_writer_mode != new.general.me_route_no_writer_mode
|
if old.general.me_route_no_writer_mode != new.general.me_route_no_writer_mode
|
||||||
|| old.general.me_route_no_writer_wait_ms != new.general.me_route_no_writer_wait_ms
|
|| old.general.me_route_no_writer_wait_ms != new.general.me_route_no_writer_wait_ms
|
||||||
|
|| old.general.me_route_hybrid_max_wait_ms != new.general.me_route_hybrid_max_wait_ms
|
||||||
|
|| old.general.me_route_blocking_send_timeout_ms
|
||||||
|
!= new.general.me_route_blocking_send_timeout_ms
|
||||||
|| old.general.me_route_inline_recovery_attempts
|
|| old.general.me_route_inline_recovery_attempts
|
||||||
!= new.general.me_route_inline_recovery_attempts
|
!= new.general.me_route_inline_recovery_attempts
|
||||||
|| old.general.me_route_inline_recovery_wait_ms
|
|| old.general.me_route_inline_recovery_wait_ms
|
||||||
@@ -634,9 +676,11 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
warned = true;
|
warned = true;
|
||||||
warn!("config reload: general.me_init_retry_attempts changed; restart required");
|
warn!("config reload: general.me_init_retry_attempts changed; restart required");
|
||||||
}
|
}
|
||||||
if old.general.me2dc_fallback != new.general.me2dc_fallback {
|
if old.general.me2dc_fallback != new.general.me2dc_fallback
|
||||||
|
|| old.general.me2dc_fast != new.general.me2dc_fast
|
||||||
|
{
|
||||||
warned = true;
|
warned = true;
|
||||||
warn!("config reload: general.me2dc_fallback changed; restart required");
|
warn!("config reload: general.me2dc_fallback/me2dc_fast changed; restart required");
|
||||||
}
|
}
|
||||||
if old.general.proxy_config_v4_cache_path != new.general.proxy_config_v4_cache_path
|
if old.general.proxy_config_v4_cache_path != new.general.proxy_config_v4_cache_path
|
||||||
|| old.general.proxy_config_v6_cache_path != new.general.proxy_config_v6_cache_path
|
|| old.general.proxy_config_v6_cache_path != new.general.proxy_config_v6_cache_path
|
||||||
@@ -655,6 +699,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
if old.general.upstream_connect_retry_attempts != new.general.upstream_connect_retry_attempts
|
if old.general.upstream_connect_retry_attempts != new.general.upstream_connect_retry_attempts
|
||||||
|| old.general.upstream_connect_retry_backoff_ms
|
|| old.general.upstream_connect_retry_backoff_ms
|
||||||
!= new.general.upstream_connect_retry_backoff_ms
|
!= new.general.upstream_connect_retry_backoff_ms
|
||||||
|
|| old.general.tg_connect != new.general.tg_connect
|
||||||
|| old.general.upstream_unhealthy_fail_threshold
|
|| old.general.upstream_unhealthy_fail_threshold
|
||||||
!= new.general.upstream_unhealthy_fail_threshold
|
!= new.general.upstream_unhealthy_fail_threshold
|
||||||
|| old.general.upstream_connect_failfast_hard_errors
|
|| old.general.upstream_connect_failfast_hard_errors
|
||||||
@@ -805,6 +850,12 @@ fn log_changes(
|
|||||||
old_hot.me_pool_drain_ttl_secs, new_hot.me_pool_drain_ttl_secs,
|
old_hot.me_pool_drain_ttl_secs, new_hot.me_pool_drain_ttl_secs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if old_hot.me_instadrain != new_hot.me_instadrain {
|
||||||
|
info!(
|
||||||
|
"config reload: me_instadrain: {} → {}",
|
||||||
|
old_hot.me_instadrain, new_hot.me_instadrain,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if old_hot.me_pool_drain_threshold != new_hot.me_pool_drain_threshold {
|
if old_hot.me_pool_drain_threshold != new_hot.me_pool_drain_threshold {
|
||||||
info!(
|
info!(
|
||||||
@@ -845,8 +896,7 @@ fn log_changes(
|
|||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"config reload: me_bind_stale: mode={:?} ttl={}s",
|
"config reload: me_bind_stale: mode={:?} ttl={}s",
|
||||||
new_hot.me_bind_stale_mode,
|
new_hot.me_bind_stale_mode, new_hot.me_bind_stale_ttl_secs
|
||||||
new_hot.me_bind_stale_ttl_secs
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if old_hot.me_secret_atomic_snapshot != new_hot.me_secret_atomic_snapshot
|
if old_hot.me_secret_atomic_snapshot != new_hot.me_secret_atomic_snapshot
|
||||||
@@ -926,8 +976,7 @@ fn log_changes(
|
|||||||
if old_hot.me_socks_kdf_policy != new_hot.me_socks_kdf_policy {
|
if old_hot.me_socks_kdf_policy != new_hot.me_socks_kdf_policy {
|
||||||
info!(
|
info!(
|
||||||
"config reload: me_socks_kdf_policy: {:?} → {:?}",
|
"config reload: me_socks_kdf_policy: {:?} → {:?}",
|
||||||
old_hot.me_socks_kdf_policy,
|
old_hot.me_socks_kdf_policy, new_hot.me_socks_kdf_policy,
|
||||||
new_hot.me_socks_kdf_policy,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -981,8 +1030,7 @@ fn log_changes(
|
|||||||
|| old_hot.me_route_backpressure_high_watermark_pct
|
|| old_hot.me_route_backpressure_high_watermark_pct
|
||||||
!= new_hot.me_route_backpressure_high_watermark_pct
|
!= new_hot.me_route_backpressure_high_watermark_pct
|
||||||
|| old_hot.me_reader_route_data_wait_ms != new_hot.me_reader_route_data_wait_ms
|
|| old_hot.me_reader_route_data_wait_ms != new_hot.me_reader_route_data_wait_ms
|
||||||
|| old_hot.me_health_interval_ms_unhealthy
|
|| old_hot.me_health_interval_ms_unhealthy != new_hot.me_health_interval_ms_unhealthy
|
||||||
!= new_hot.me_health_interval_ms_unhealthy
|
|
||||||
|| old_hot.me_health_interval_ms_healthy != new_hot.me_health_interval_ms_healthy
|
|| old_hot.me_health_interval_ms_healthy != new_hot.me_health_interval_ms_healthy
|
||||||
|| old_hot.me_admission_poll_ms != new_hot.me_admission_poll_ms
|
|| old_hot.me_admission_poll_ms != new_hot.me_admission_poll_ms
|
||||||
|| old_hot.me_warn_rate_limit_ms != new_hot.me_warn_rate_limit_ms
|
|| old_hot.me_warn_rate_limit_ms != new_hot.me_warn_rate_limit_ms
|
||||||
@@ -1004,34 +1052,47 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_hot.users != new_hot.users {
|
if old_hot.users != new_hot.users {
|
||||||
let mut added: Vec<&String> = new_hot.users.keys()
|
let mut added: Vec<&String> = new_hot
|
||||||
|
.users
|
||||||
|
.keys()
|
||||||
.filter(|u| !old_hot.users.contains_key(*u))
|
.filter(|u| !old_hot.users.contains_key(*u))
|
||||||
.collect();
|
.collect();
|
||||||
added.sort();
|
added.sort();
|
||||||
|
|
||||||
let mut removed: Vec<&String> = old_hot.users.keys()
|
let mut removed: Vec<&String> = old_hot
|
||||||
|
.users
|
||||||
|
.keys()
|
||||||
.filter(|u| !new_hot.users.contains_key(*u))
|
.filter(|u| !new_hot.users.contains_key(*u))
|
||||||
.collect();
|
.collect();
|
||||||
removed.sort();
|
removed.sort();
|
||||||
|
|
||||||
let mut changed: Vec<&String> = new_hot.users.keys()
|
let mut changed: Vec<&String> = new_hot
|
||||||
|
.users
|
||||||
|
.keys()
|
||||||
.filter(|u| {
|
.filter(|u| {
|
||||||
old_hot.users.get(*u)
|
old_hot
|
||||||
|
.users
|
||||||
|
.get(*u)
|
||||||
.map(|s| s != &new_hot.users[*u])
|
.map(|s| s != &new_hot.users[*u])
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
@@ -1041,10 +1102,18 @@ fn log_changes(
|
|||||||
if !added.is_empty() {
|
if !added.is_empty() {
|
||||||
info!(
|
info!(
|
||||||
"config reload: users added: [{}]",
|
"config reload: users added: [{}]",
|
||||||
added.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
|
added
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
);
|
);
|
||||||
let host = resolve_link_host(new_cfg, detected_ip_v4, detected_ip_v6);
|
let host = resolve_link_host(new_cfg, detected_ip_v4, detected_ip_v6);
|
||||||
let port = new_cfg.general.links.public_port.unwrap_or(new_cfg.server.port);
|
let port = new_cfg
|
||||||
|
.general
|
||||||
|
.links
|
||||||
|
.public_port
|
||||||
|
.unwrap_or(new_cfg.server.port);
|
||||||
for user in &added {
|
for user in &added {
|
||||||
if let Some(secret) = new_hot.users.get(*user) {
|
if let Some(secret) = new_hot.users.get(*user) {
|
||||||
print_user_links(user, secret, &host, port, new_cfg);
|
print_user_links(user, secret, &host, port, new_cfg);
|
||||||
@@ -1054,13 +1123,21 @@ fn log_changes(
|
|||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
info!(
|
info!(
|
||||||
"config reload: users removed: [{}]",
|
"config reload: users removed: [{}]",
|
||||||
removed.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
|
removed
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !changed.is_empty() {
|
if !changed.is_empty() {
|
||||||
info!(
|
info!(
|
||||||
"config reload: users secret changed: [{}]",
|
"config reload: users secret changed: [{}]",
|
||||||
changed.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
|
changed
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1071,6 +1148,12 @@ fn log_changes(
|
|||||||
new_hot.user_max_tcp_conns.len()
|
new_hot.user_max_tcp_conns.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if old_hot.user_max_tcp_conns_global_each != new_hot.user_max_tcp_conns_global_each {
|
||||||
|
info!(
|
||||||
|
"config reload: user_max_tcp_conns policy global_each={}",
|
||||||
|
new_hot.user_max_tcp_conns_global_each
|
||||||
|
);
|
||||||
|
}
|
||||||
if old_hot.user_expirations != new_hot.user_expirations {
|
if old_hot.user_expirations != new_hot.user_expirations {
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_expirations updated ({} entries)",
|
"config reload: user_expirations updated ({} entries)",
|
||||||
@@ -1091,8 +1174,7 @@ fn log_changes(
|
|||||||
}
|
}
|
||||||
if old_hot.user_max_unique_ips_global_each != new_hot.user_max_unique_ips_global_each
|
if old_hot.user_max_unique_ips_global_each != new_hot.user_max_unique_ips_global_each
|
||||||
|| old_hot.user_max_unique_ips_mode != new_hot.user_max_unique_ips_mode
|
|| old_hot.user_max_unique_ips_mode != new_hot.user_max_unique_ips_mode
|
||||||
|| old_hot.user_max_unique_ips_window_secs
|
|| old_hot.user_max_unique_ips_window_secs != new_hot.user_max_unique_ips_window_secs
|
||||||
!= new_hot.user_max_unique_ips_window_secs
|
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_max_unique_ips policy global_each={} mode={:?} window={}s",
|
"config reload: user_max_unique_ips policy global_each={} mode={:?} window={}s",
|
||||||
@@ -1127,7 +1209,10 @@ fn reload_config(
|
|||||||
let next_manifest = WatchManifest::from_source_files(&source_files);
|
let next_manifest = WatchManifest::from_source_files(&source_files);
|
||||||
|
|
||||||
if let Err(e) = new_cfg.validate() {
|
if let Err(e) = new_cfg.validate() {
|
||||||
error!("config reload: validation failed: {}; keeping old config", e);
|
error!(
|
||||||
|
"config reload: validation failed: {}; keeping old config",
|
||||||
|
e
|
||||||
|
);
|
||||||
return Some(next_manifest);
|
return Some(next_manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1192,7 +1277,7 @@ pub fn spawn_config_watcher(
|
|||||||
) -> (watch::Receiver<Arc<ProxyConfig>>, watch::Receiver<LogLevel>) {
|
) -> (watch::Receiver<Arc<ProxyConfig>>, watch::Receiver<LogLevel>) {
|
||||||
let initial_level = initial.general.log_level.clone();
|
let initial_level = initial.general.log_level.clone();
|
||||||
let (config_tx, config_rx) = watch::channel(initial);
|
let (config_tx, config_rx) = watch::channel(initial);
|
||||||
let (log_tx, log_rx) = watch::channel(initial_level);
|
let (log_tx, log_rx) = watch::channel(initial_level);
|
||||||
|
|
||||||
let config_path = normalize_watch_path(&config_path);
|
let config_path = normalize_watch_path(&config_path);
|
||||||
let initial_loaded = ProxyConfig::load_with_metadata(&config_path).ok();
|
let initial_loaded = ProxyConfig::load_with_metadata(&config_path).ok();
|
||||||
@@ -1209,25 +1294,29 @@ pub fn spawn_config_watcher(
|
|||||||
|
|
||||||
let tx_inotify = notify_tx.clone();
|
let tx_inotify = notify_tx.clone();
|
||||||
let manifest_for_inotify = manifest_state.clone();
|
let manifest_for_inotify = manifest_state.clone();
|
||||||
let mut inotify_watcher = match recommended_watcher(move |res: notify::Result<notify::Event>| {
|
let mut inotify_watcher =
|
||||||
let Ok(event) = res else { return };
|
match recommended_watcher(move |res: notify::Result<notify::Event>| {
|
||||||
if !matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
|
let Ok(event) = res else { return };
|
||||||
return;
|
if !matches!(
|
||||||
}
|
event.kind,
|
||||||
let is_our_file = manifest_for_inotify
|
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
|
||||||
.read()
|
) {
|
||||||
.map(|manifest| manifest.matches_event_paths(&event.paths))
|
return;
|
||||||
.unwrap_or(false);
|
}
|
||||||
if is_our_file {
|
let is_our_file = manifest_for_inotify
|
||||||
let _ = tx_inotify.try_send(());
|
.read()
|
||||||
}
|
.map(|manifest| manifest.matches_event_paths(&event.paths))
|
||||||
}) {
|
.unwrap_or(false);
|
||||||
Ok(watcher) => Some(watcher),
|
if is_our_file {
|
||||||
Err(e) => {
|
let _ = tx_inotify.try_send(());
|
||||||
warn!("config watcher: inotify unavailable: {}", e);
|
}
|
||||||
None
|
}) {
|
||||||
}
|
Ok(watcher) => Some(watcher),
|
||||||
};
|
Err(e) => {
|
||||||
|
warn!("config watcher: inotify unavailable: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
apply_watch_manifest(
|
apply_watch_manifest(
|
||||||
inotify_watcher.as_mut(),
|
inotify_watcher.as_mut(),
|
||||||
Option::<&mut notify::poll::PollWatcher>::None,
|
Option::<&mut notify::poll::PollWatcher>::None,
|
||||||
@@ -1243,7 +1332,10 @@ pub fn spawn_config_watcher(
|
|||||||
let mut poll_watcher = match notify::poll::PollWatcher::new(
|
let mut poll_watcher = match notify::poll::PollWatcher::new(
|
||||||
move |res: notify::Result<notify::Event>| {
|
move |res: notify::Result<notify::Event>| {
|
||||||
let Ok(event) = res else { return };
|
let Ok(event) = res else { return };
|
||||||
if !matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
|
if !matches!(
|
||||||
|
event.kind,
|
||||||
|
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let is_our_file = manifest_for_poll
|
let is_our_file = manifest_for_poll
|
||||||
@@ -1291,7 +1383,9 @@ pub fn spawn_config_watcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
if notify_rx.recv().await.is_none() { break; }
|
if notify_rx.recv().await.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Debounce: drain extra events that arrive within a short quiet window.
|
// Debounce: drain extra events that arrive within a short quiet window.
|
||||||
tokio::time::sleep(HOT_RELOAD_DEBOUNCE).await;
|
tokio::time::sleep(HOT_RELOAD_DEBOUNCE).await;
|
||||||
@@ -1393,7 +1487,10 @@ mod tests {
|
|||||||
new.server.port = old.server.port.saturating_add(1);
|
new.server.port = old.server.port.saturating_add(1);
|
||||||
|
|
||||||
let applied = overlay_hot_fields(&old, &new);
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
assert_eq!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
assert_eq!(
|
||||||
|
HotFields::from_config(&old),
|
||||||
|
HotFields::from_config(&applied)
|
||||||
|
);
|
||||||
assert_eq!(applied.server.port, old.server.port);
|
assert_eq!(applied.server.port, old.server.port);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1412,7 +1509,10 @@ mod tests {
|
|||||||
applied.general.me_bind_stale_mode,
|
applied.general.me_bind_stale_mode,
|
||||||
new.general.me_bind_stale_mode
|
new.general.me_bind_stale_mode
|
||||||
);
|
);
|
||||||
assert_ne!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
assert_ne!(
|
||||||
|
HotFields::from_config(&old),
|
||||||
|
HotFields::from_config(&applied)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1426,7 +1526,10 @@ mod tests {
|
|||||||
applied.general.me_keepalive_interval_secs,
|
applied.general.me_keepalive_interval_secs,
|
||||||
old.general.me_keepalive_interval_secs
|
old.general.me_keepalive_interval_secs
|
||||||
);
|
);
|
||||||
assert_eq!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
assert_eq!(
|
||||||
|
HotFields::from_config(&old),
|
||||||
|
HotFields::from_config(&applied)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1438,7 +1541,10 @@ mod tests {
|
|||||||
|
|
||||||
let applied = overlay_hot_fields(&old, &new);
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
assert_eq!(applied.general.hardswap, new.general.hardswap);
|
assert_eq!(applied.general.hardswap, new.general.hardswap);
|
||||||
assert_eq!(applied.general.use_middle_proxy, old.general.use_middle_proxy);
|
assert_eq!(
|
||||||
|
applied.general.use_middle_proxy,
|
||||||
|
old.general.use_middle_proxy
|
||||||
|
);
|
||||||
assert!(!config_equal(&applied, &new));
|
assert!(!config_equal(&applied, &new));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1450,14 +1556,19 @@ mod tests {
|
|||||||
|
|
||||||
write_reload_config(&path, Some(initial_tag), None);
|
write_reload_config(&path, Some(initial_tag), None);
|
||||||
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
||||||
let initial_hash = ProxyConfig::load_with_metadata(&path).unwrap().rendered_hash;
|
let initial_hash = ProxyConfig::load_with_metadata(&path)
|
||||||
|
.unwrap()
|
||||||
|
.rendered_hash;
|
||||||
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
||||||
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
||||||
let mut reload_state = ReloadState::new(Some(initial_hash));
|
let mut reload_state = ReloadState::new(Some(initial_hash));
|
||||||
|
|
||||||
write_reload_config(&path, Some(final_tag), None);
|
write_reload_config(&path, Some(final_tag), None);
|
||||||
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
||||||
assert_eq!(config_tx.borrow().general.ad_tag.as_deref(), Some(final_tag));
|
assert_eq!(
|
||||||
|
config_tx.borrow().general.ad_tag.as_deref(),
|
||||||
|
Some(final_tag)
|
||||||
|
);
|
||||||
|
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
@@ -1470,7 +1581,9 @@ mod tests {
|
|||||||
|
|
||||||
write_reload_config(&path, Some(initial_tag), None);
|
write_reload_config(&path, Some(initial_tag), None);
|
||||||
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
||||||
let initial_hash = ProxyConfig::load_with_metadata(&path).unwrap().rendered_hash;
|
let initial_hash = ProxyConfig::load_with_metadata(&path)
|
||||||
|
.unwrap()
|
||||||
|
.rendered_hash;
|
||||||
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
||||||
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
||||||
let mut reload_state = ReloadState::new(Some(initial_hash));
|
let mut reload_state = ReloadState::new(Some(initial_hash));
|
||||||
@@ -1493,7 +1606,9 @@ mod tests {
|
|||||||
|
|
||||||
write_reload_config(&path, Some(initial_tag), None);
|
write_reload_config(&path, Some(initial_tag), None);
|
||||||
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
||||||
let initial_hash = ProxyConfig::load_with_metadata(&path).unwrap().rendered_hash;
|
let initial_hash = ProxyConfig::load_with_metadata(&path)
|
||||||
|
.unwrap()
|
||||||
|
.rendered_hash;
|
||||||
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
||||||
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
||||||
let mut reload_state = ReloadState::new(Some(initial_hash));
|
let mut reload_state = ReloadState::new(Some(initial_hash));
|
||||||
@@ -1507,7 +1622,10 @@ mod tests {
|
|||||||
|
|
||||||
write_reload_config(&path, Some(final_tag), None);
|
write_reload_config(&path, Some(final_tag), None);
|
||||||
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
||||||
assert_eq!(config_tx.borrow().general.ad_tag.as_deref(), Some(final_tag));
|
assert_eq!(
|
||||||
|
config_tx.borrow().general.ad_tag.as_deref(),
|
||||||
|
Some(final_tag)
|
||||||
|
);
|
||||||
|
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|||||||
+1004
-36
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
//! Configuration.
|
//! Configuration.
|
||||||
|
|
||||||
pub(crate) mod defaults;
|
pub(crate) mod defaults;
|
||||||
mod types;
|
|
||||||
mod load;
|
|
||||||
pub mod hot_reload;
|
pub mod hot_reload;
|
||||||
|
mod load;
|
||||||
|
mod types;
|
||||||
|
|
||||||
pub use load::ProxyConfig;
|
pub use load::ProxyConfig;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn write_temp_config(contents: &str) -> PathBuf {
|
||||||
|
let nonce = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("system time must be after unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
let path = std::env::temp_dir().join(format!("telemt-idle-policy-{nonce}.toml"));
|
||||||
|
fs::write(&path, contents).expect("temp config write must succeed");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_temp_config(path: &PathBuf) {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_timeouts_enable_apple_compatible_handshake_profile() {
|
||||||
|
let cfg = ProxyConfig::default();
|
||||||
|
assert_eq!(cfg.timeouts.client_first_byte_idle_secs, 300);
|
||||||
|
assert_eq!(cfg.timeouts.client_handshake, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_accepts_zero_first_byte_idle_timeout_as_legacy_opt_out() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[timeouts]
|
||||||
|
client_first_byte_idle_secs = 0
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = ProxyConfig::load(&path).expect("config with zero first-byte idle timeout must load");
|
||||||
|
assert_eq!(cfg.timeouts.client_first_byte_idle_secs, 0);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_relay_hard_idle_smaller_than_soft_idle_with_clear_error() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[timeouts]
|
||||||
|
relay_client_idle_soft_secs = 120
|
||||||
|
relay_client_idle_hard_secs = 60
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ProxyConfig::load(&path).expect_err("config with hard<soft must fail");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains(
|
||||||
|
"timeouts.relay_client_idle_hard_secs must be >= timeouts.relay_client_idle_soft_secs"
|
||||||
|
),
|
||||||
|
"error must explain the violated hard>=soft invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_relay_grace_larger_than_hard_idle_with_clear_error() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[timeouts]
|
||||||
|
relay_client_idle_soft_secs = 60
|
||||||
|
relay_client_idle_hard_secs = 120
|
||||||
|
relay_idle_grace_after_downstream_activity_secs = 121
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ProxyConfig::load(&path).expect_err("config with grace>hard must fail");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("timeouts.relay_idle_grace_after_downstream_activity_secs must be <= timeouts.relay_client_idle_hard_secs"),
|
||||||
|
"error must explain the violated grace<=hard invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_zero_handshake_timeout_with_clear_error() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[timeouts]
|
||||||
|
client_handshake = 0
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ProxyConfig::load(&path).expect_err("config with zero handshake timeout must fail");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("timeouts.client_handshake must be > 0"),
|
||||||
|
"error must explain that handshake timeout must be positive, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn write_temp_config(contents: &str) -> PathBuf {
|
||||||
|
let nonce = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("system time must be after unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"telemt-load-mask-prefetch-timeout-security-{nonce}.toml"
|
||||||
|
));
|
||||||
|
fs::write(&path, contents).expect("temp config write must succeed");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_temp_config(path: &PathBuf) {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_mask_classifier_prefetch_timeout_below_min_bound() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_classifier_prefetch_timeout_ms = 4
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ProxyConfig::load(&path)
|
||||||
|
.expect_err("prefetch timeout below minimum security bound must be rejected");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_classifier_prefetch_timeout_ms must be within [5, 50]"),
|
||||||
|
"error must explain timeout bound invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_mask_classifier_prefetch_timeout_above_max_bound() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_classifier_prefetch_timeout_ms = 51
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ProxyConfig::load(&path)
|
||||||
|
.expect_err("prefetch timeout above max security bound must be rejected");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_classifier_prefetch_timeout_ms must be within [5, 50]"),
|
||||||
|
"error must explain timeout bound invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_accepts_mask_classifier_prefetch_timeout_within_bounds() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_classifier_prefetch_timeout_ms = 20
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg =
|
||||||
|
ProxyConfig::load(&path).expect("prefetch timeout within security bounds must be accepted");
|
||||||
|
assert_eq!(cfg.censorship.mask_classifier_prefetch_timeout_ms, 20);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn write_temp_config(contents: &str) -> PathBuf {
|
||||||
|
let nonce = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("system time must be after unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
let path = std::env::temp_dir().join(format!("telemt-load-mask-shape-security-{nonce}.toml"));
|
||||||
|
fs::write(&path, contents).expect("temp config write must succeed");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_temp_config(path: &PathBuf) {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_zero_mask_shape_bucket_floor_bytes() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_shape_bucket_floor_bytes = 0
|
||||||
|
mask_shape_bucket_cap_bytes = 4096
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ProxyConfig::load(&path).expect_err("zero mask_shape_bucket_floor_bytes must be rejected");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_shape_bucket_floor_bytes must be > 0"),
|
||||||
|
"error must explain floor>0 invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_mask_shape_bucket_cap_less_than_floor() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_shape_bucket_floor_bytes = 1024
|
||||||
|
mask_shape_bucket_cap_bytes = 512
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ProxyConfig::load(&path).expect_err("mask_shape_bucket_cap_bytes < floor must be rejected");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains(
|
||||||
|
"censorship.mask_shape_bucket_cap_bytes must be >= censorship.mask_shape_bucket_floor_bytes"
|
||||||
|
),
|
||||||
|
"error must explain cap>=floor invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_accepts_mask_shape_bucket_cap_equal_to_floor() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_shape_hardening = true
|
||||||
|
mask_shape_bucket_floor_bytes = 1024
|
||||||
|
mask_shape_bucket_cap_bytes = 1024
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = ProxyConfig::load(&path).expect("equal cap and floor must be accepted");
|
||||||
|
assert!(cfg.censorship.mask_shape_hardening);
|
||||||
|
assert_eq!(cfg.censorship.mask_shape_bucket_floor_bytes, 1024);
|
||||||
|
assert_eq!(cfg.censorship.mask_shape_bucket_cap_bytes, 1024);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_above_cap_blur_when_shape_hardening_disabled() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_shape_hardening = false
|
||||||
|
mask_shape_above_cap_blur = true
|
||||||
|
mask_shape_above_cap_blur_max_bytes = 64
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ProxyConfig::load(&path).expect_err("above-cap blur must require shape hardening enabled");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains(
|
||||||
|
"censorship.mask_shape_above_cap_blur requires censorship.mask_shape_hardening = true"
|
||||||
|
),
|
||||||
|
"error must explain blur prerequisite, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_above_cap_blur_with_zero_max_bytes() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_shape_hardening = true
|
||||||
|
mask_shape_above_cap_blur = true
|
||||||
|
mask_shape_above_cap_blur_max_bytes = 0
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ProxyConfig::load(&path).expect_err("above-cap blur max bytes must be > 0 when enabled");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_shape_above_cap_blur_max_bytes must be > 0 when censorship.mask_shape_above_cap_blur is enabled"),
|
||||||
|
"error must explain blur max bytes invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_timing_normalization_floor_zero_when_enabled() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_timing_normalization_enabled = true
|
||||||
|
mask_timing_normalization_floor_ms = 0
|
||||||
|
mask_timing_normalization_ceiling_ms = 200
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ProxyConfig::load(&path).expect_err("timing normalization floor must be > 0 when enabled");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_timing_normalization_floor_ms must be > 0 when censorship.mask_timing_normalization_enabled is true"),
|
||||||
|
"error must explain timing floor invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_timing_normalization_ceiling_below_floor() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_timing_normalization_enabled = true
|
||||||
|
mask_timing_normalization_floor_ms = 220
|
||||||
|
mask_timing_normalization_ceiling_ms = 200
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ProxyConfig::load(&path).expect_err("timing normalization ceiling must be >= floor");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_timing_normalization_ceiling_ms must be >= censorship.mask_timing_normalization_floor_ms"),
|
||||||
|
"error must explain timing ceiling/floor invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_accepts_valid_timing_normalization_and_above_cap_blur_config() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_shape_hardening = true
|
||||||
|
mask_shape_above_cap_blur = true
|
||||||
|
mask_shape_above_cap_blur_max_bytes = 128
|
||||||
|
mask_timing_normalization_enabled = true
|
||||||
|
mask_timing_normalization_floor_ms = 150
|
||||||
|
mask_timing_normalization_ceiling_ms = 240
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = ProxyConfig::load(&path)
|
||||||
|
.expect("valid blur and timing normalization settings must be accepted");
|
||||||
|
assert!(cfg.censorship.mask_shape_hardening);
|
||||||
|
assert!(cfg.censorship.mask_shape_above_cap_blur);
|
||||||
|
assert_eq!(cfg.censorship.mask_shape_above_cap_blur_max_bytes, 128);
|
||||||
|
assert!(cfg.censorship.mask_timing_normalization_enabled);
|
||||||
|
assert_eq!(cfg.censorship.mask_timing_normalization_floor_ms, 150);
|
||||||
|
assert_eq!(cfg.censorship.mask_timing_normalization_ceiling_ms, 240);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_aggressive_shape_mode_when_shape_hardening_disabled() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_shape_hardening = false
|
||||||
|
mask_shape_hardening_aggressive_mode = true
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ProxyConfig::load(&path)
|
||||||
|
.expect_err("aggressive shape hardening mode must require shape hardening enabled");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_shape_hardening_aggressive_mode requires censorship.mask_shape_hardening = true"),
|
||||||
|
"error must explain aggressive-mode prerequisite, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_accepts_aggressive_shape_mode_when_shape_hardening_enabled() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_shape_hardening = true
|
||||||
|
mask_shape_hardening_aggressive_mode = true
|
||||||
|
mask_shape_above_cap_blur = true
|
||||||
|
mask_shape_above_cap_blur_max_bytes = 8
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = ProxyConfig::load(&path)
|
||||||
|
.expect("aggressive shape hardening mode should be accepted when prerequisites are met");
|
||||||
|
assert!(cfg.censorship.mask_shape_hardening);
|
||||||
|
assert!(cfg.censorship.mask_shape_hardening_aggressive_mode);
|
||||||
|
assert!(cfg.censorship.mask_shape_above_cap_blur);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_zero_mask_relay_max_bytes() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_relay_max_bytes = 0
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = ProxyConfig::load(&path).expect_err("mask_relay_max_bytes must be > 0");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_relay_max_bytes must be > 0"),
|
||||||
|
"error must explain non-zero relay cap invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_mask_relay_max_bytes_above_upper_bound() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_relay_max_bytes = 67108865
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ProxyConfig::load(&path).expect_err("mask_relay_max_bytes above hard cap must be rejected");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("censorship.mask_relay_max_bytes must be <= 67108864"),
|
||||||
|
"error must explain relay cap upper bound invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_accepts_valid_mask_relay_max_bytes() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[censorship]
|
||||||
|
mask_relay_max_bytes = 8388608
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = ProxyConfig::load(&path).expect("valid mask_relay_max_bytes must be accepted");
|
||||||
|
assert_eq!(cfg.censorship.mask_relay_max_bytes, 8_388_608);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn write_temp_config(contents: &str) -> PathBuf {
|
||||||
|
let nonce = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("system time must be after unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
let path = std::env::temp_dir().join(format!("telemt-load-security-{nonce}.toml"));
|
||||||
|
fs::write(&path, contents).expect("temp config write must succeed");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_temp_config(path: &PathBuf) {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_server_hello_delay_equal_to_handshake_timeout_budget() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[timeouts]
|
||||||
|
client_handshake = 1
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
server_hello_delay_max_ms = 1000
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ProxyConfig::load(&path).expect_err("delay equal to handshake timeout must be rejected");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains(
|
||||||
|
"censorship.server_hello_delay_max_ms must be < timeouts.client_handshake * 1000"
|
||||||
|
),
|
||||||
|
"error must explain delay<timeout invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_server_hello_delay_larger_than_handshake_timeout_budget() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[timeouts]
|
||||||
|
client_handshake = 1
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
server_hello_delay_max_ms = 1500
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ProxyConfig::load(&path).expect_err("delay larger than handshake timeout must be rejected");
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains(
|
||||||
|
"censorship.server_hello_delay_max_ms must be < timeouts.client_handshake * 1000"
|
||||||
|
),
|
||||||
|
"error must explain delay<timeout invariant, got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_accepts_server_hello_delay_strictly_below_handshake_timeout_budget() {
|
||||||
|
let path = write_temp_config(
|
||||||
|
r#"
|
||||||
|
[timeouts]
|
||||||
|
client_handshake = 1
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
server_hello_delay_max_ms = 999
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg =
|
||||||
|
ProxyConfig::load(&path).expect("delay below handshake timeout budget must be accepted");
|
||||||
|
assert_eq!(cfg.timeouts.client_handshake, 1);
|
||||||
|
assert_eq!(cfg.censorship.server_hello_delay_max_ms, 999);
|
||||||
|
|
||||||
|
remove_temp_config(&path);
|
||||||
|
}
|
||||||
+461
-36
@@ -135,8 +135,8 @@ impl MeSocksKdfPolicy {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum MeBindStaleMode {
|
pub enum MeBindStaleMode {
|
||||||
Never,
|
|
||||||
#[default]
|
#[default]
|
||||||
|
Never,
|
||||||
Ttl,
|
Ttl,
|
||||||
Always,
|
Always,
|
||||||
}
|
}
|
||||||
@@ -429,6 +429,11 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_me2dc_fallback")]
|
#[serde(default = "default_me2dc_fallback")]
|
||||||
pub me2dc_fallback: bool,
|
pub me2dc_fallback: bool,
|
||||||
|
|
||||||
|
/// Fast ME->Direct fallback mode for new sessions.
|
||||||
|
/// Active only when both `use_middle_proxy=true` and `me2dc_fallback=true`.
|
||||||
|
#[serde(default = "default_me2dc_fast")]
|
||||||
|
pub me2dc_fast: bool,
|
||||||
|
|
||||||
/// Enable ME keepalive padding frames.
|
/// Enable ME keepalive padding frames.
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub me_keepalive_enabled: bool,
|
pub me_keepalive_enabled: bool,
|
||||||
@@ -462,8 +467,13 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_me_c2me_channel_capacity")]
|
#[serde(default = "default_me_c2me_channel_capacity")]
|
||||||
pub me_c2me_channel_capacity: usize,
|
pub me_c2me_channel_capacity: usize,
|
||||||
|
|
||||||
|
/// Maximum wait in milliseconds for enqueueing C2ME commands when the queue is full.
|
||||||
|
/// `0` keeps legacy unbounded wait behavior.
|
||||||
|
#[serde(default = "default_me_c2me_send_timeout_ms")]
|
||||||
|
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,
|
||||||
|
|
||||||
@@ -484,6 +494,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,
|
||||||
@@ -645,6 +663,10 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_upstream_connect_budget_ms")]
|
#[serde(default = "default_upstream_connect_budget_ms")]
|
||||||
pub upstream_connect_budget_ms: u64,
|
pub upstream_connect_budget_ms: u64,
|
||||||
|
|
||||||
|
/// Per-attempt TCP connect timeout to Telegram DC (seconds).
|
||||||
|
#[serde(default = "default_connect_timeout")]
|
||||||
|
pub tg_connect: u64,
|
||||||
|
|
||||||
/// Consecutive failed requests before upstream is marked unhealthy.
|
/// Consecutive failed requests before upstream is marked unhealthy.
|
||||||
#[serde(default = "default_upstream_unhealthy_fail_threshold")]
|
#[serde(default = "default_upstream_unhealthy_fail_threshold")]
|
||||||
pub upstream_unhealthy_fail_threshold: u32,
|
pub upstream_unhealthy_fail_threshold: u32,
|
||||||
@@ -716,6 +738,15 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_me_route_no_writer_wait_ms")]
|
#[serde(default = "default_me_route_no_writer_wait_ms")]
|
||||||
pub me_route_no_writer_wait_ms: u64,
|
pub me_route_no_writer_wait_ms: u64,
|
||||||
|
|
||||||
|
/// Maximum cumulative wait in milliseconds for hybrid no-writer mode before failfast.
|
||||||
|
#[serde(default = "default_me_route_hybrid_max_wait_ms")]
|
||||||
|
pub me_route_hybrid_max_wait_ms: u64,
|
||||||
|
|
||||||
|
/// Maximum wait in milliseconds for blocking ME writer channel send fallback.
|
||||||
|
/// `0` keeps legacy unbounded wait behavior.
|
||||||
|
#[serde(default = "default_me_route_blocking_send_timeout_ms")]
|
||||||
|
pub me_route_blocking_send_timeout_ms: u64,
|
||||||
|
|
||||||
/// Number of inline recovery attempts in legacy mode.
|
/// Number of inline recovery attempts in legacy mode.
|
||||||
#[serde(default = "default_me_route_inline_recovery_attempts")]
|
#[serde(default = "default_me_route_inline_recovery_attempts")]
|
||||||
pub me_route_inline_recovery_attempts: u32,
|
pub me_route_inline_recovery_attempts: u32,
|
||||||
@@ -798,11 +829,35 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_me_pool_drain_ttl_secs")]
|
#[serde(default = "default_me_pool_drain_ttl_secs")]
|
||||||
pub me_pool_drain_ttl_secs: u64,
|
pub me_pool_drain_ttl_secs: u64,
|
||||||
|
|
||||||
|
/// Force-remove any draining writer on the next cleanup tick, regardless of age/deadline.
|
||||||
|
#[serde(default = "default_me_instadrain")]
|
||||||
|
pub me_instadrain: bool,
|
||||||
|
|
||||||
/// Maximum allowed number of draining ME writers before oldest ones are force-closed in batches.
|
/// Maximum allowed number of draining ME writers before oldest ones are force-closed in batches.
|
||||||
/// Set to 0 to disable threshold-based draining cleanup and keep timeout-only behavior.
|
/// Set to 0 to disable threshold-based draining cleanup and keep timeout-only behavior.
|
||||||
#[serde(default = "default_me_pool_drain_threshold")]
|
#[serde(default = "default_me_pool_drain_threshold")]
|
||||||
pub me_pool_drain_threshold: u64,
|
pub me_pool_drain_threshold: u64,
|
||||||
|
|
||||||
|
/// Enable staged client eviction for draining ME writers that remain non-empty past TTL.
|
||||||
|
#[serde(default = "default_me_pool_drain_soft_evict_enabled")]
|
||||||
|
pub me_pool_drain_soft_evict_enabled: bool,
|
||||||
|
|
||||||
|
/// Extra grace in seconds after drain TTL before soft-eviction stage starts.
|
||||||
|
#[serde(default = "default_me_pool_drain_soft_evict_grace_secs")]
|
||||||
|
pub me_pool_drain_soft_evict_grace_secs: u64,
|
||||||
|
|
||||||
|
/// Maximum number of client sessions to evict from one draining writer per health tick.
|
||||||
|
#[serde(default = "default_me_pool_drain_soft_evict_per_writer")]
|
||||||
|
pub me_pool_drain_soft_evict_per_writer: u8,
|
||||||
|
|
||||||
|
/// Soft-eviction budget per CPU core for one health tick.
|
||||||
|
#[serde(default = "default_me_pool_drain_soft_evict_budget_per_core")]
|
||||||
|
pub me_pool_drain_soft_evict_budget_per_core: u16,
|
||||||
|
|
||||||
|
/// Cooldown for repetitive soft-eviction on the same writer in milliseconds.
|
||||||
|
#[serde(default = "default_me_pool_drain_soft_evict_cooldown_ms")]
|
||||||
|
pub me_pool_drain_soft_evict_cooldown_ms: u64,
|
||||||
|
|
||||||
/// Policy for new binds on stale draining writers.
|
/// Policy for new binds on stale draining writers.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub me_bind_stale_mode: MeBindStaleMode,
|
pub me_bind_stale_mode: MeBindStaleMode,
|
||||||
@@ -817,7 +872,7 @@ pub struct GeneralConfig {
|
|||||||
pub me_pool_min_fresh_ratio: f32,
|
pub me_pool_min_fresh_ratio: f32,
|
||||||
|
|
||||||
/// Drain timeout in seconds for stale ME writers after endpoint map changes.
|
/// Drain timeout in seconds for stale ME writers after endpoint map changes.
|
||||||
/// Set to 0 to keep stale writers draining indefinitely (no force-close).
|
/// Set to 0 to use the runtime safety fallback timeout.
|
||||||
#[serde(default = "default_me_reinit_drain_timeout_secs")]
|
#[serde(default = "default_me_reinit_drain_timeout_secs")]
|
||||||
pub me_reinit_drain_timeout_secs: u64,
|
pub me_reinit_drain_timeout_secs: u64,
|
||||||
|
|
||||||
@@ -893,6 +948,7 @@ impl Default for GeneralConfig {
|
|||||||
middle_proxy_warm_standby: default_middle_proxy_warm_standby(),
|
middle_proxy_warm_standby: default_middle_proxy_warm_standby(),
|
||||||
me_init_retry_attempts: default_me_init_retry_attempts(),
|
me_init_retry_attempts: default_me_init_retry_attempts(),
|
||||||
me2dc_fallback: default_me2dc_fallback(),
|
me2dc_fallback: default_me2dc_fallback(),
|
||||||
|
me2dc_fast: default_me2dc_fast(),
|
||||||
me_keepalive_enabled: default_true(),
|
me_keepalive_enabled: default_true(),
|
||||||
me_keepalive_interval_secs: default_keepalive_interval(),
|
me_keepalive_interval_secs: default_keepalive_interval(),
|
||||||
me_keepalive_jitter_secs: default_keepalive_jitter(),
|
me_keepalive_jitter_secs: default_keepalive_jitter(),
|
||||||
@@ -901,11 +957,15 @@ impl Default for GeneralConfig {
|
|||||||
me_writer_cmd_channel_capacity: default_me_writer_cmd_channel_capacity(),
|
me_writer_cmd_channel_capacity: default_me_writer_cmd_channel_capacity(),
|
||||||
me_route_channel_capacity: default_me_route_channel_capacity(),
|
me_route_channel_capacity: default_me_route_channel_capacity(),
|
||||||
me_c2me_channel_capacity: default_me_c2me_channel_capacity(),
|
me_c2me_channel_capacity: default_me_c2me_channel_capacity(),
|
||||||
|
me_c2me_send_timeout_ms: default_me_c2me_send_timeout_ms(),
|
||||||
me_reader_route_data_wait_ms: default_me_reader_route_data_wait_ms(),
|
me_reader_route_data_wait_ms: default_me_reader_route_data_wait_ms(),
|
||||||
me_d2c_flush_batch_max_frames: default_me_d2c_flush_batch_max_frames(),
|
me_d2c_flush_batch_max_frames: default_me_d2c_flush_batch_max_frames(),
|
||||||
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(),
|
||||||
@@ -916,27 +976,42 @@ impl Default for GeneralConfig {
|
|||||||
me_reconnect_backoff_cap_ms: default_reconnect_backoff_cap_ms(),
|
me_reconnect_backoff_cap_ms: default_reconnect_backoff_cap_ms(),
|
||||||
me_reconnect_fast_retry_count: default_me_reconnect_fast_retry_count(),
|
me_reconnect_fast_retry_count: default_me_reconnect_fast_retry_count(),
|
||||||
me_single_endpoint_shadow_writers: default_me_single_endpoint_shadow_writers(),
|
me_single_endpoint_shadow_writers: default_me_single_endpoint_shadow_writers(),
|
||||||
me_single_endpoint_outage_mode_enabled: default_me_single_endpoint_outage_mode_enabled(),
|
me_single_endpoint_outage_mode_enabled: default_me_single_endpoint_outage_mode_enabled(
|
||||||
me_single_endpoint_outage_disable_quarantine: default_me_single_endpoint_outage_disable_quarantine(),
|
),
|
||||||
me_single_endpoint_outage_backoff_min_ms: default_me_single_endpoint_outage_backoff_min_ms(),
|
me_single_endpoint_outage_disable_quarantine:
|
||||||
me_single_endpoint_outage_backoff_max_ms: default_me_single_endpoint_outage_backoff_max_ms(),
|
default_me_single_endpoint_outage_disable_quarantine(),
|
||||||
me_single_endpoint_shadow_rotate_every_secs: default_me_single_endpoint_shadow_rotate_every_secs(),
|
me_single_endpoint_outage_backoff_min_ms:
|
||||||
|
default_me_single_endpoint_outage_backoff_min_ms(),
|
||||||
|
me_single_endpoint_outage_backoff_max_ms:
|
||||||
|
default_me_single_endpoint_outage_backoff_max_ms(),
|
||||||
|
me_single_endpoint_shadow_rotate_every_secs:
|
||||||
|
default_me_single_endpoint_shadow_rotate_every_secs(),
|
||||||
me_floor_mode: MeFloorMode::default(),
|
me_floor_mode: MeFloorMode::default(),
|
||||||
me_adaptive_floor_idle_secs: default_me_adaptive_floor_idle_secs(),
|
me_adaptive_floor_idle_secs: default_me_adaptive_floor_idle_secs(),
|
||||||
me_adaptive_floor_min_writers_single_endpoint: default_me_adaptive_floor_min_writers_single_endpoint(),
|
me_adaptive_floor_min_writers_single_endpoint:
|
||||||
me_adaptive_floor_min_writers_multi_endpoint: default_me_adaptive_floor_min_writers_multi_endpoint(),
|
default_me_adaptive_floor_min_writers_single_endpoint(),
|
||||||
|
me_adaptive_floor_min_writers_multi_endpoint:
|
||||||
|
default_me_adaptive_floor_min_writers_multi_endpoint(),
|
||||||
me_adaptive_floor_recover_grace_secs: default_me_adaptive_floor_recover_grace_secs(),
|
me_adaptive_floor_recover_grace_secs: default_me_adaptive_floor_recover_grace_secs(),
|
||||||
me_adaptive_floor_writers_per_core_total: default_me_adaptive_floor_writers_per_core_total(),
|
me_adaptive_floor_writers_per_core_total:
|
||||||
|
default_me_adaptive_floor_writers_per_core_total(),
|
||||||
me_adaptive_floor_cpu_cores_override: default_me_adaptive_floor_cpu_cores_override(),
|
me_adaptive_floor_cpu_cores_override: default_me_adaptive_floor_cpu_cores_override(),
|
||||||
me_adaptive_floor_max_extra_writers_single_per_core: default_me_adaptive_floor_max_extra_writers_single_per_core(),
|
me_adaptive_floor_max_extra_writers_single_per_core:
|
||||||
me_adaptive_floor_max_extra_writers_multi_per_core: default_me_adaptive_floor_max_extra_writers_multi_per_core(),
|
default_me_adaptive_floor_max_extra_writers_single_per_core(),
|
||||||
me_adaptive_floor_max_active_writers_per_core: default_me_adaptive_floor_max_active_writers_per_core(),
|
me_adaptive_floor_max_extra_writers_multi_per_core:
|
||||||
me_adaptive_floor_max_warm_writers_per_core: default_me_adaptive_floor_max_warm_writers_per_core(),
|
default_me_adaptive_floor_max_extra_writers_multi_per_core(),
|
||||||
me_adaptive_floor_max_active_writers_global: default_me_adaptive_floor_max_active_writers_global(),
|
me_adaptive_floor_max_active_writers_per_core:
|
||||||
me_adaptive_floor_max_warm_writers_global: default_me_adaptive_floor_max_warm_writers_global(),
|
default_me_adaptive_floor_max_active_writers_per_core(),
|
||||||
|
me_adaptive_floor_max_warm_writers_per_core:
|
||||||
|
default_me_adaptive_floor_max_warm_writers_per_core(),
|
||||||
|
me_adaptive_floor_max_active_writers_global:
|
||||||
|
default_me_adaptive_floor_max_active_writers_global(),
|
||||||
|
me_adaptive_floor_max_warm_writers_global:
|
||||||
|
default_me_adaptive_floor_max_warm_writers_global(),
|
||||||
upstream_connect_retry_attempts: default_upstream_connect_retry_attempts(),
|
upstream_connect_retry_attempts: default_upstream_connect_retry_attempts(),
|
||||||
upstream_connect_retry_backoff_ms: default_upstream_connect_retry_backoff_ms(),
|
upstream_connect_retry_backoff_ms: default_upstream_connect_retry_backoff_ms(),
|
||||||
upstream_connect_budget_ms: default_upstream_connect_budget_ms(),
|
upstream_connect_budget_ms: default_upstream_connect_budget_ms(),
|
||||||
|
tg_connect: default_connect_timeout(),
|
||||||
upstream_unhealthy_fail_threshold: default_upstream_unhealthy_fail_threshold(),
|
upstream_unhealthy_fail_threshold: default_upstream_unhealthy_fail_threshold(),
|
||||||
upstream_connect_failfast_hard_errors: default_upstream_connect_failfast_hard_errors(),
|
upstream_connect_failfast_hard_errors: default_upstream_connect_failfast_hard_errors(),
|
||||||
stun_iface_mismatch_ignore: false,
|
stun_iface_mismatch_ignore: false,
|
||||||
@@ -948,13 +1023,16 @@ impl Default for GeneralConfig {
|
|||||||
me_socks_kdf_policy: MeSocksKdfPolicy::Strict,
|
me_socks_kdf_policy: MeSocksKdfPolicy::Strict,
|
||||||
me_route_backpressure_base_timeout_ms: default_me_route_backpressure_base_timeout_ms(),
|
me_route_backpressure_base_timeout_ms: default_me_route_backpressure_base_timeout_ms(),
|
||||||
me_route_backpressure_high_timeout_ms: default_me_route_backpressure_high_timeout_ms(),
|
me_route_backpressure_high_timeout_ms: default_me_route_backpressure_high_timeout_ms(),
|
||||||
me_route_backpressure_high_watermark_pct: default_me_route_backpressure_high_watermark_pct(),
|
me_route_backpressure_high_watermark_pct:
|
||||||
|
default_me_route_backpressure_high_watermark_pct(),
|
||||||
me_health_interval_ms_unhealthy: default_me_health_interval_ms_unhealthy(),
|
me_health_interval_ms_unhealthy: default_me_health_interval_ms_unhealthy(),
|
||||||
me_health_interval_ms_healthy: default_me_health_interval_ms_healthy(),
|
me_health_interval_ms_healthy: default_me_health_interval_ms_healthy(),
|
||||||
me_admission_poll_ms: default_me_admission_poll_ms(),
|
me_admission_poll_ms: default_me_admission_poll_ms(),
|
||||||
me_warn_rate_limit_ms: default_me_warn_rate_limit_ms(),
|
me_warn_rate_limit_ms: default_me_warn_rate_limit_ms(),
|
||||||
me_route_no_writer_mode: MeRouteNoWriterMode::default(),
|
me_route_no_writer_mode: MeRouteNoWriterMode::default(),
|
||||||
me_route_no_writer_wait_ms: default_me_route_no_writer_wait_ms(),
|
me_route_no_writer_wait_ms: default_me_route_no_writer_wait_ms(),
|
||||||
|
me_route_hybrid_max_wait_ms: default_me_route_hybrid_max_wait_ms(),
|
||||||
|
me_route_blocking_send_timeout_ms: default_me_route_blocking_send_timeout_ms(),
|
||||||
me_route_inline_recovery_attempts: default_me_route_inline_recovery_attempts(),
|
me_route_inline_recovery_attempts: default_me_route_inline_recovery_attempts(),
|
||||||
me_route_inline_recovery_wait_ms: default_me_route_inline_recovery_wait_ms(),
|
me_route_inline_recovery_wait_ms: default_me_route_inline_recovery_wait_ms(),
|
||||||
links: LinksConfig::default(),
|
links: LinksConfig::default(),
|
||||||
@@ -972,7 +1050,8 @@ impl Default for GeneralConfig {
|
|||||||
me_hardswap_warmup_delay_min_ms: default_me_hardswap_warmup_delay_min_ms(),
|
me_hardswap_warmup_delay_min_ms: default_me_hardswap_warmup_delay_min_ms(),
|
||||||
me_hardswap_warmup_delay_max_ms: default_me_hardswap_warmup_delay_max_ms(),
|
me_hardswap_warmup_delay_max_ms: default_me_hardswap_warmup_delay_max_ms(),
|
||||||
me_hardswap_warmup_extra_passes: default_me_hardswap_warmup_extra_passes(),
|
me_hardswap_warmup_extra_passes: default_me_hardswap_warmup_extra_passes(),
|
||||||
me_hardswap_warmup_pass_backoff_base_ms: default_me_hardswap_warmup_pass_backoff_base_ms(),
|
me_hardswap_warmup_pass_backoff_base_ms:
|
||||||
|
default_me_hardswap_warmup_pass_backoff_base_ms(),
|
||||||
me_config_stable_snapshots: default_me_config_stable_snapshots(),
|
me_config_stable_snapshots: default_me_config_stable_snapshots(),
|
||||||
me_config_apply_cooldown_secs: default_me_config_apply_cooldown_secs(),
|
me_config_apply_cooldown_secs: default_me_config_apply_cooldown_secs(),
|
||||||
me_snapshot_require_http_2xx: default_me_snapshot_require_http_2xx(),
|
me_snapshot_require_http_2xx: default_me_snapshot_require_http_2xx(),
|
||||||
@@ -983,7 +1062,14 @@ impl Default for GeneralConfig {
|
|||||||
me_secret_atomic_snapshot: default_me_secret_atomic_snapshot(),
|
me_secret_atomic_snapshot: default_me_secret_atomic_snapshot(),
|
||||||
proxy_secret_len_max: default_proxy_secret_len_max(),
|
proxy_secret_len_max: default_proxy_secret_len_max(),
|
||||||
me_pool_drain_ttl_secs: default_me_pool_drain_ttl_secs(),
|
me_pool_drain_ttl_secs: default_me_pool_drain_ttl_secs(),
|
||||||
|
me_instadrain: default_me_instadrain(),
|
||||||
me_pool_drain_threshold: default_me_pool_drain_threshold(),
|
me_pool_drain_threshold: default_me_pool_drain_threshold(),
|
||||||
|
me_pool_drain_soft_evict_enabled: default_me_pool_drain_soft_evict_enabled(),
|
||||||
|
me_pool_drain_soft_evict_grace_secs: default_me_pool_drain_soft_evict_grace_secs(),
|
||||||
|
me_pool_drain_soft_evict_per_writer: default_me_pool_drain_soft_evict_per_writer(),
|
||||||
|
me_pool_drain_soft_evict_budget_per_core:
|
||||||
|
default_me_pool_drain_soft_evict_budget_per_core(),
|
||||||
|
me_pool_drain_soft_evict_cooldown_ms: default_me_pool_drain_soft_evict_cooldown_ms(),
|
||||||
me_bind_stale_mode: MeBindStaleMode::default(),
|
me_bind_stale_mode: MeBindStaleMode::default(),
|
||||||
me_bind_stale_ttl_secs: default_me_bind_stale_ttl_secs(),
|
me_bind_stale_ttl_secs: default_me_bind_stale_ttl_secs(),
|
||||||
me_pool_min_fresh_ratio: default_me_pool_min_fresh_ratio(),
|
me_pool_min_fresh_ratio: default_me_pool_min_fresh_ratio(),
|
||||||
@@ -1008,8 +1094,10 @@ impl GeneralConfig {
|
|||||||
/// Resolve the active updater interval for ME infrastructure refresh tasks.
|
/// Resolve the active updater interval for ME infrastructure refresh tasks.
|
||||||
/// `update_every` has priority, otherwise legacy proxy_*_auto_reload_secs are used.
|
/// `update_every` has priority, otherwise legacy proxy_*_auto_reload_secs are used.
|
||||||
pub fn effective_update_every_secs(&self) -> u64 {
|
pub fn effective_update_every_secs(&self) -> u64 {
|
||||||
self.update_every
|
self.update_every.unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| self.proxy_secret_auto_reload_secs.min(self.proxy_config_auto_reload_secs))
|
self.proxy_secret_auto_reload_secs
|
||||||
|
.min(self.proxy_config_auto_reload_secs)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve periodic zero-downtime reinit interval for ME writers.
|
/// Resolve periodic zero-downtime reinit interval for ME writers.
|
||||||
@@ -1019,8 +1107,13 @@ impl GeneralConfig {
|
|||||||
|
|
||||||
/// Resolve force-close timeout for stale writers.
|
/// Resolve force-close timeout for stale writers.
|
||||||
/// `me_reinit_drain_timeout_secs` remains backward-compatible alias.
|
/// `me_reinit_drain_timeout_secs` remains backward-compatible alias.
|
||||||
|
/// A configured `0` uses the runtime safety fallback (300s).
|
||||||
pub fn effective_me_pool_force_close_secs(&self) -> u64 {
|
pub fn effective_me_pool_force_close_secs(&self) -> u64 {
|
||||||
self.me_reinit_drain_timeout_secs
|
if self.me_reinit_drain_timeout_secs == 0 {
|
||||||
|
300
|
||||||
|
} else {
|
||||||
|
self.me_reinit_drain_timeout_secs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,6 +1216,118 @@ impl Default for ApiConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ConntrackMode {
|
||||||
|
#[default]
|
||||||
|
Tracked,
|
||||||
|
Notrack,
|
||||||
|
Hybrid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ConntrackBackend {
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
Nftables,
|
||||||
|
Iptables,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ConntrackPressureProfile {
|
||||||
|
Conservative,
|
||||||
|
#[default]
|
||||||
|
Balanced,
|
||||||
|
Aggressive,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConntrackPressureProfile {
|
||||||
|
pub fn client_first_byte_idle_cap_secs(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::Conservative => 30,
|
||||||
|
Self::Balanced => 20,
|
||||||
|
Self::Aggressive => 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn direct_activity_timeout_secs(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::Conservative => 180,
|
||||||
|
Self::Balanced => 120,
|
||||||
|
Self::Aggressive => 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn middle_soft_idle_cap_secs(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::Conservative => 60,
|
||||||
|
Self::Balanced => 30,
|
||||||
|
Self::Aggressive => 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn middle_hard_idle_cap_secs(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::Conservative => 180,
|
||||||
|
Self::Balanced => 90,
|
||||||
|
Self::Aggressive => 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConntrackControlConfig {
|
||||||
|
/// Enables runtime conntrack-control worker for pressure mitigation.
|
||||||
|
#[serde(default = "default_conntrack_control_enabled")]
|
||||||
|
pub inline_conntrack_control: bool,
|
||||||
|
|
||||||
|
/// Conntrack mode for listener ingress traffic.
|
||||||
|
#[serde(default)]
|
||||||
|
pub mode: ConntrackMode,
|
||||||
|
|
||||||
|
/// Netfilter backend used to reconcile notrack rules.
|
||||||
|
#[serde(default)]
|
||||||
|
pub backend: ConntrackBackend,
|
||||||
|
|
||||||
|
/// Pressure profile for timeout caps under resource saturation.
|
||||||
|
#[serde(default)]
|
||||||
|
pub profile: ConntrackPressureProfile,
|
||||||
|
|
||||||
|
/// Listener IP allow-list for hybrid mode.
|
||||||
|
/// Ignored in tracked/notrack mode.
|
||||||
|
#[serde(default)]
|
||||||
|
pub hybrid_listener_ips: Vec<IpAddr>,
|
||||||
|
|
||||||
|
/// Pressure high watermark as percentage.
|
||||||
|
#[serde(default = "default_conntrack_pressure_high_watermark_pct")]
|
||||||
|
pub pressure_high_watermark_pct: u8,
|
||||||
|
|
||||||
|
/// Pressure low watermark as percentage.
|
||||||
|
#[serde(default = "default_conntrack_pressure_low_watermark_pct")]
|
||||||
|
pub pressure_low_watermark_pct: u8,
|
||||||
|
|
||||||
|
/// Maximum conntrack delete operations per second.
|
||||||
|
#[serde(default = "default_conntrack_delete_budget_per_sec")]
|
||||||
|
pub delete_budget_per_sec: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConntrackControlConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inline_conntrack_control: default_conntrack_control_enabled(),
|
||||||
|
mode: ConntrackMode::default(),
|
||||||
|
backend: ConntrackBackend::default(),
|
||||||
|
profile: ConntrackPressureProfile::default(),
|
||||||
|
hybrid_listener_ips: Vec::new(),
|
||||||
|
pressure_high_watermark_pct: default_conntrack_pressure_high_watermark_pct(),
|
||||||
|
pressure_low_watermark_pct: default_conntrack_pressure_low_watermark_pct(),
|
||||||
|
delete_budget_per_sec: default_conntrack_delete_budget_per_sec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
#[serde(default = "default_port")]
|
#[serde(default = "default_port")]
|
||||||
@@ -1158,9 +1363,10 @@ pub struct ServerConfig {
|
|||||||
|
|
||||||
/// Trusted source CIDRs allowed to send incoming PROXY protocol headers.
|
/// Trusted source CIDRs allowed to send incoming PROXY protocol headers.
|
||||||
///
|
///
|
||||||
/// When non-empty, connections from addresses outside this allowlist are
|
/// If this field is omitted in config, it defaults to trust-all CIDRs
|
||||||
/// rejected before `src_addr` is applied.
|
/// (`0.0.0.0/0` and `::/0`). If it is explicitly set to an empty list,
|
||||||
#[serde(default)]
|
/// all PROXY protocol headers are rejected.
|
||||||
|
#[serde(default = "default_proxy_protocol_trusted_cidrs")]
|
||||||
pub proxy_protocol_trusted_cidrs: Vec<IpNetwork>,
|
pub proxy_protocol_trusted_cidrs: Vec<IpNetwork>,
|
||||||
|
|
||||||
/// Port for the Prometheus-compatible metrics endpoint.
|
/// Port for the Prometheus-compatible metrics endpoint.
|
||||||
@@ -1183,10 +1389,24 @@ pub struct ServerConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub listeners: Vec<ListenerConfig>,
|
pub listeners: Vec<ListenerConfig>,
|
||||||
|
|
||||||
|
/// TCP `listen(2)` backlog for client-facing sockets (also used for the metrics HTTP listener).
|
||||||
|
/// The effective queue is capped by the kernel (for example `somaxconn` on Linux).
|
||||||
|
#[serde(default = "default_listen_backlog")]
|
||||||
|
pub listen_backlog: u32,
|
||||||
|
|
||||||
/// Maximum number of concurrent client connections.
|
/// Maximum number of concurrent client connections.
|
||||||
/// 0 means unlimited.
|
/// 0 means unlimited.
|
||||||
#[serde(default = "default_server_max_connections")]
|
#[serde(default = "default_server_max_connections")]
|
||||||
pub max_connections: u32,
|
pub max_connections: u32,
|
||||||
|
|
||||||
|
/// Maximum wait in milliseconds while acquiring a connection slot permit.
|
||||||
|
/// `0` keeps legacy unbounded wait behavior.
|
||||||
|
#[serde(default = "default_accept_permit_timeout_ms")]
|
||||||
|
pub accept_permit_timeout_ms: u64,
|
||||||
|
|
||||||
|
/// Runtime conntrack control and pressure policy.
|
||||||
|
#[serde(default)]
|
||||||
|
pub conntrack_control: ConntrackControlConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerConfig {
|
impl Default for ServerConfig {
|
||||||
@@ -1200,24 +1420,48 @@ impl Default for ServerConfig {
|
|||||||
listen_tcp: None,
|
listen_tcp: None,
|
||||||
proxy_protocol: false,
|
proxy_protocol: false,
|
||||||
proxy_protocol_header_timeout_ms: default_proxy_protocol_header_timeout_ms(),
|
proxy_protocol_header_timeout_ms: default_proxy_protocol_header_timeout_ms(),
|
||||||
proxy_protocol_trusted_cidrs: Vec::new(),
|
proxy_protocol_trusted_cidrs: default_proxy_protocol_trusted_cidrs(),
|
||||||
metrics_port: None,
|
metrics_port: None,
|
||||||
metrics_listen: None,
|
metrics_listen: None,
|
||||||
metrics_whitelist: default_metrics_whitelist(),
|
metrics_whitelist: default_metrics_whitelist(),
|
||||||
api: ApiConfig::default(),
|
api: ApiConfig::default(),
|
||||||
listeners: Vec::new(),
|
listeners: Vec::new(),
|
||||||
|
listen_backlog: default_listen_backlog(),
|
||||||
max_connections: default_server_max_connections(),
|
max_connections: default_server_max_connections(),
|
||||||
|
accept_permit_timeout_ms: default_accept_permit_timeout_ms(),
|
||||||
|
conntrack_control: ConntrackControlConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TimeoutsConfig {
|
pub struct TimeoutsConfig {
|
||||||
|
/// Maximum idle wait in seconds for the first client byte before handshake parsing starts.
|
||||||
|
/// `0` disables the separate idle phase and keeps legacy timeout behavior.
|
||||||
|
#[serde(default = "default_client_first_byte_idle_secs")]
|
||||||
|
pub client_first_byte_idle_secs: u64,
|
||||||
|
|
||||||
|
/// Maximum active handshake duration in seconds after the first client byte is received.
|
||||||
#[serde(default = "default_handshake_timeout")]
|
#[serde(default = "default_handshake_timeout")]
|
||||||
pub client_handshake: u64,
|
pub client_handshake: u64,
|
||||||
|
|
||||||
#[serde(default = "default_connect_timeout")]
|
/// Enables soft/hard relay client idle policy for middle-relay sessions.
|
||||||
pub tg_connect: u64,
|
#[serde(default = "default_relay_idle_policy_v2_enabled")]
|
||||||
|
pub relay_idle_policy_v2_enabled: bool,
|
||||||
|
|
||||||
|
/// Soft idle threshold for middle-relay client uplink activity in seconds.
|
||||||
|
/// Hitting this threshold marks the session as idle-candidate, but does not close it.
|
||||||
|
#[serde(default = "default_relay_client_idle_soft_secs")]
|
||||||
|
pub relay_client_idle_soft_secs: u64,
|
||||||
|
|
||||||
|
/// Hard idle threshold for middle-relay client uplink activity in seconds.
|
||||||
|
/// Hitting this threshold closes the session.
|
||||||
|
#[serde(default = "default_relay_client_idle_hard_secs")]
|
||||||
|
pub relay_client_idle_hard_secs: u64,
|
||||||
|
|
||||||
|
/// Additional grace in seconds added to hard idle window after recent downstream activity.
|
||||||
|
#[serde(default = "default_relay_idle_grace_after_downstream_activity_secs")]
|
||||||
|
pub relay_idle_grace_after_downstream_activity_secs: u64,
|
||||||
|
|
||||||
#[serde(default = "default_keepalive")]
|
#[serde(default = "default_keepalive")]
|
||||||
pub client_keepalive: u64,
|
pub client_keepalive: u64,
|
||||||
@@ -1237,8 +1481,13 @@ pub struct TimeoutsConfig {
|
|||||||
impl Default for TimeoutsConfig {
|
impl Default for TimeoutsConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
client_first_byte_idle_secs: default_client_first_byte_idle_secs(),
|
||||||
client_handshake: default_handshake_timeout(),
|
client_handshake: default_handshake_timeout(),
|
||||||
tg_connect: default_connect_timeout(),
|
relay_idle_policy_v2_enabled: default_relay_idle_policy_v2_enabled(),
|
||||||
|
relay_client_idle_soft_secs: default_relay_client_idle_soft_secs(),
|
||||||
|
relay_client_idle_hard_secs: default_relay_client_idle_hard_secs(),
|
||||||
|
relay_idle_grace_after_downstream_activity_secs:
|
||||||
|
default_relay_idle_grace_after_downstream_activity_secs(),
|
||||||
client_keepalive: default_keepalive(),
|
client_keepalive: default_keepalive(),
|
||||||
client_ack: default_ack_timeout(),
|
client_ack: default_ack_timeout(),
|
||||||
me_one_retry: default_me_one_retry(),
|
me_one_retry: default_me_one_retry(),
|
||||||
@@ -1247,6 +1496,90 @@ impl Default for TimeoutsConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum UnknownSniAction {
|
||||||
|
#[default]
|
||||||
|
Drop,
|
||||||
|
Mask,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum TlsFetchProfile {
|
||||||
|
ModernChromeLike,
|
||||||
|
ModernFirefoxLike,
|
||||||
|
CompatTls12,
|
||||||
|
LegacyMinimal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TlsFetchProfile {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TlsFetchProfile::ModernChromeLike => "modern_chrome_like",
|
||||||
|
TlsFetchProfile::ModernFirefoxLike => "modern_firefox_like",
|
||||||
|
TlsFetchProfile::CompatTls12 => "compat_tls12",
|
||||||
|
TlsFetchProfile::LegacyMinimal => "legacy_minimal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_tls_fetch_profiles() -> Vec<TlsFetchProfile> {
|
||||||
|
vec![
|
||||||
|
TlsFetchProfile::ModernChromeLike,
|
||||||
|
TlsFetchProfile::ModernFirefoxLike,
|
||||||
|
TlsFetchProfile::CompatTls12,
|
||||||
|
TlsFetchProfile::LegacyMinimal,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TlsFetchConfig {
|
||||||
|
/// Ordered list of ClientHello profiles used for adaptive fallback.
|
||||||
|
#[serde(default = "default_tls_fetch_profiles")]
|
||||||
|
pub profiles: Vec<TlsFetchProfile>,
|
||||||
|
|
||||||
|
/// When true and upstream route is configured, TLS fetch fails closed on
|
||||||
|
/// upstream connect errors and does not fallback to direct TCP.
|
||||||
|
#[serde(default = "default_tls_fetch_strict_route")]
|
||||||
|
pub strict_route: bool,
|
||||||
|
|
||||||
|
/// Timeout per one profile attempt in milliseconds.
|
||||||
|
#[serde(default = "default_tls_fetch_attempt_timeout_ms")]
|
||||||
|
pub attempt_timeout_ms: u64,
|
||||||
|
|
||||||
|
/// Total wall-clock budget in milliseconds across all profile attempts.
|
||||||
|
#[serde(default = "default_tls_fetch_total_budget_ms")]
|
||||||
|
pub total_budget_ms: u64,
|
||||||
|
|
||||||
|
/// Adds GREASE-style values into selected ClientHello extensions.
|
||||||
|
#[serde(default)]
|
||||||
|
pub grease_enabled: bool,
|
||||||
|
|
||||||
|
/// Produces deterministic ClientHello randomness for debugging/tests.
|
||||||
|
#[serde(default)]
|
||||||
|
pub deterministic: bool,
|
||||||
|
|
||||||
|
/// TTL for winner-profile cache entries in seconds.
|
||||||
|
/// Set to 0 to disable profile cache.
|
||||||
|
#[serde(default = "default_tls_fetch_profile_cache_ttl_secs")]
|
||||||
|
pub profile_cache_ttl_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TlsFetchConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
profiles: default_tls_fetch_profiles(),
|
||||||
|
strict_route: default_tls_fetch_strict_route(),
|
||||||
|
attempt_timeout_ms: default_tls_fetch_attempt_timeout_ms(),
|
||||||
|
total_budget_ms: default_tls_fetch_total_budget_ms(),
|
||||||
|
grease_enabled: false,
|
||||||
|
deterministic: false,
|
||||||
|
profile_cache_ttl_secs: default_tls_fetch_profile_cache_ttl_secs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AntiCensorshipConfig {
|
pub struct AntiCensorshipConfig {
|
||||||
#[serde(default = "default_tls_domain")]
|
#[serde(default = "default_tls_domain")]
|
||||||
@@ -1256,6 +1589,19 @@ pub struct AntiCensorshipConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tls_domains: Vec<String>,
|
pub tls_domains: Vec<String>,
|
||||||
|
|
||||||
|
/// Policy for TLS ClientHello with unknown (non-configured) SNI.
|
||||||
|
#[serde(default)]
|
||||||
|
pub unknown_sni_action: UnknownSniAction,
|
||||||
|
|
||||||
|
/// Upstream scope used for TLS front metadata fetches.
|
||||||
|
/// Empty value keeps default upstream routing behavior.
|
||||||
|
#[serde(default = "default_tls_fetch_scope")]
|
||||||
|
pub tls_fetch_scope: String,
|
||||||
|
|
||||||
|
/// Fetch strategy for TLS front metadata bootstrap and periodic refresh.
|
||||||
|
#[serde(default)]
|
||||||
|
pub tls_fetch: TlsFetchConfig,
|
||||||
|
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub mask: bool,
|
pub mask: bool,
|
||||||
|
|
||||||
@@ -1306,6 +1652,54 @@ pub struct AntiCensorshipConfig {
|
|||||||
/// Allows the backend to see the real client IP.
|
/// Allows the backend to see the real client IP.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mask_proxy_protocol: u8,
|
pub mask_proxy_protocol: u8,
|
||||||
|
|
||||||
|
/// Enable shape-channel hardening on mask backend path by padding
|
||||||
|
/// client->mask stream tail to configured buckets on stream end.
|
||||||
|
#[serde(default = "default_mask_shape_hardening")]
|
||||||
|
pub mask_shape_hardening: bool,
|
||||||
|
|
||||||
|
/// Opt-in aggressive shape hardening mode.
|
||||||
|
/// When enabled, masking may shape some backend-silent timeout paths and
|
||||||
|
/// enforces strictly positive above-cap blur when blur is enabled.
|
||||||
|
#[serde(default = "default_mask_shape_hardening_aggressive_mode")]
|
||||||
|
pub mask_shape_hardening_aggressive_mode: bool,
|
||||||
|
|
||||||
|
/// Minimum bucket size for mask shape hardening padding.
|
||||||
|
#[serde(default = "default_mask_shape_bucket_floor_bytes")]
|
||||||
|
pub mask_shape_bucket_floor_bytes: usize,
|
||||||
|
|
||||||
|
/// Maximum bucket size for mask shape hardening padding.
|
||||||
|
#[serde(default = "default_mask_shape_bucket_cap_bytes")]
|
||||||
|
pub mask_shape_bucket_cap_bytes: usize,
|
||||||
|
|
||||||
|
/// Add bounded random tail bytes even when total bytes already exceed
|
||||||
|
/// mask_shape_bucket_cap_bytes.
|
||||||
|
#[serde(default = "default_mask_shape_above_cap_blur")]
|
||||||
|
pub mask_shape_above_cap_blur: bool,
|
||||||
|
|
||||||
|
/// Maximum random bytes appended above cap when above-cap blur is enabled.
|
||||||
|
#[serde(default = "default_mask_shape_above_cap_blur_max_bytes")]
|
||||||
|
pub mask_shape_above_cap_blur_max_bytes: usize,
|
||||||
|
|
||||||
|
/// Maximum bytes relayed per direction on unauthenticated masking fallback paths.
|
||||||
|
#[serde(default = "default_mask_relay_max_bytes")]
|
||||||
|
pub mask_relay_max_bytes: usize,
|
||||||
|
|
||||||
|
/// Prefetch timeout (ms) for extending fragmented masking classifier window.
|
||||||
|
#[serde(default = "default_mask_classifier_prefetch_timeout_ms")]
|
||||||
|
pub mask_classifier_prefetch_timeout_ms: u64,
|
||||||
|
|
||||||
|
/// Enable outcome-time normalization envelope for masking fallback.
|
||||||
|
#[serde(default = "default_mask_timing_normalization_enabled")]
|
||||||
|
pub mask_timing_normalization_enabled: bool,
|
||||||
|
|
||||||
|
/// Lower bound (ms) for masking outcome timing envelope.
|
||||||
|
#[serde(default = "default_mask_timing_normalization_floor_ms")]
|
||||||
|
pub mask_timing_normalization_floor_ms: u64,
|
||||||
|
|
||||||
|
/// Upper bound (ms) for masking outcome timing envelope.
|
||||||
|
#[serde(default = "default_mask_timing_normalization_ceiling_ms")]
|
||||||
|
pub mask_timing_normalization_ceiling_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AntiCensorshipConfig {
|
impl Default for AntiCensorshipConfig {
|
||||||
@@ -1313,6 +1707,9 @@ impl Default for AntiCensorshipConfig {
|
|||||||
Self {
|
Self {
|
||||||
tls_domain: default_tls_domain(),
|
tls_domain: default_tls_domain(),
|
||||||
tls_domains: Vec::new(),
|
tls_domains: Vec::new(),
|
||||||
|
unknown_sni_action: UnknownSniAction::Drop,
|
||||||
|
tls_fetch_scope: default_tls_fetch_scope(),
|
||||||
|
tls_fetch: TlsFetchConfig::default(),
|
||||||
mask: default_true(),
|
mask: default_true(),
|
||||||
mask_host: None,
|
mask_host: None,
|
||||||
mask_port: default_mask_port(),
|
mask_port: default_mask_port(),
|
||||||
@@ -1326,6 +1723,17 @@ impl Default for AntiCensorshipConfig {
|
|||||||
tls_full_cert_ttl_secs: default_tls_full_cert_ttl_secs(),
|
tls_full_cert_ttl_secs: default_tls_full_cert_ttl_secs(),
|
||||||
alpn_enforce: default_alpn_enforce(),
|
alpn_enforce: default_alpn_enforce(),
|
||||||
mask_proxy_protocol: 0,
|
mask_proxy_protocol: 0,
|
||||||
|
mask_shape_hardening: default_mask_shape_hardening(),
|
||||||
|
mask_shape_hardening_aggressive_mode: default_mask_shape_hardening_aggressive_mode(),
|
||||||
|
mask_shape_bucket_floor_bytes: default_mask_shape_bucket_floor_bytes(),
|
||||||
|
mask_shape_bucket_cap_bytes: default_mask_shape_bucket_cap_bytes(),
|
||||||
|
mask_shape_above_cap_blur: default_mask_shape_above_cap_blur(),
|
||||||
|
mask_shape_above_cap_blur_max_bytes: default_mask_shape_above_cap_blur_max_bytes(),
|
||||||
|
mask_relay_max_bytes: default_mask_relay_max_bytes(),
|
||||||
|
mask_classifier_prefetch_timeout_ms: default_mask_classifier_prefetch_timeout_ms(),
|
||||||
|
mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(),
|
||||||
|
mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(),
|
||||||
|
mask_timing_normalization_ceiling_ms: default_mask_timing_normalization_ceiling_ms(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1342,6 +1750,12 @@ pub struct AccessConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub user_max_tcp_conns: HashMap<String, usize>,
|
pub user_max_tcp_conns: HashMap<String, usize>,
|
||||||
|
|
||||||
|
/// Global per-user TCP connection limit applied when a user has no
|
||||||
|
/// positive individual override.
|
||||||
|
/// `0` disables the inherited limit.
|
||||||
|
#[serde(default = "default_user_max_tcp_conns_global_each")]
|
||||||
|
pub user_max_tcp_conns_global_each: usize,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub user_expirations: HashMap<String, DateTime<Utc>>,
|
pub user_expirations: HashMap<String, DateTime<Utc>>,
|
||||||
|
|
||||||
@@ -1378,6 +1792,7 @@ impl Default for AccessConfig {
|
|||||||
users: default_access_users(),
|
users: default_access_users(),
|
||||||
user_ad_tags: HashMap::new(),
|
user_ad_tags: HashMap::new(),
|
||||||
user_max_tcp_conns: HashMap::new(),
|
user_max_tcp_conns: HashMap::new(),
|
||||||
|
user_max_tcp_conns_global_each: default_user_max_tcp_conns_global_each(),
|
||||||
user_expirations: HashMap::new(),
|
user_expirations: HashMap::new(),
|
||||||
user_data_quota: HashMap::new(),
|
user_data_quota: HashMap::new(),
|
||||||
user_max_unique_ips: HashMap::new(),
|
user_max_unique_ips: HashMap::new(),
|
||||||
@@ -1418,6 +1833,11 @@ pub enum UpstreamType {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
},
|
},
|
||||||
|
Shadowsocks {
|
||||||
|
url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
interface: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -1498,7 +1918,10 @@ impl ShowLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for ShowLink {
|
impl Serialize for ShowLink {
|
||||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
|
fn serialize<S: serde::Serializer>(
|
||||||
|
&self,
|
||||||
|
serializer: S,
|
||||||
|
) -> std::result::Result<S::Ok, S::Error> {
|
||||||
match self {
|
match self {
|
||||||
ShowLink::None => Vec::<String>::new().serialize(serializer),
|
ShowLink::None => Vec::<String>::new().serialize(serializer),
|
||||||
ShowLink::All => serializer.serialize_str("*"),
|
ShowLink::All => serializer.serialize_str("*"),
|
||||||
@@ -1508,7 +1931,9 @@ impl Serialize for ShowLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for ShowLink {
|
impl<'de> Deserialize<'de> for ShowLink {
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
|
fn deserialize<D: serde::Deserializer<'de>>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> std::result::Result<Self, D::Error> {
|
||||||
use serde::de;
|
use serde::de;
|
||||||
|
|
||||||
struct ShowLinkVisitor;
|
struct ShowLinkVisitor;
|
||||||
@@ -1524,14 +1949,14 @@ impl<'de> Deserialize<'de> for ShowLink {
|
|||||||
if v == "*" {
|
if v == "*" {
|
||||||
Ok(ShowLink::All)
|
Ok(ShowLink::All)
|
||||||
} else {
|
} else {
|
||||||
Err(de::Error::invalid_value(
|
Err(de::Error::invalid_value(de::Unexpected::Str(v), &r#""*""#))
|
||||||
de::Unexpected::Str(v),
|
|
||||||
&r#""*""#,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> std::result::Result<ShowLink, A::Error> {
|
fn visit_seq<A: de::SeqAccess<'de>>(
|
||||||
|
self,
|
||||||
|
mut seq: A,
|
||||||
|
) -> std::result::Result<ShowLink, A::Error> {
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
while let Some(name) = seq.next_element::<String>()? {
|
while let Some(name) = seq.next_element::<String>()? {
|
||||||
names.push(name);
|
names.push(name);
|
||||||
|
|||||||
@@ -0,0 +1,755 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tokio::sync::{mpsc, watch};
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::config::{ConntrackBackend, ConntrackMode, ProxyConfig};
|
||||||
|
use crate::proxy::middle_relay::note_global_relay_pressure;
|
||||||
|
use crate::proxy::shared_state::{ConntrackCloseEvent, ConntrackCloseReason, ProxySharedState};
|
||||||
|
use crate::stats::Stats;
|
||||||
|
|
||||||
|
const CONNTRACK_EVENT_QUEUE_CAPACITY: usize = 32_768;
|
||||||
|
const PRESSURE_RELEASE_TICKS: u8 = 3;
|
||||||
|
const PRESSURE_SAMPLE_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum NetfilterBackend {
|
||||||
|
Nftables,
|
||||||
|
Iptables,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct PressureSample {
|
||||||
|
conn_pct: Option<u8>,
|
||||||
|
fd_pct: Option<u8>,
|
||||||
|
accept_timeout_delta: u64,
|
||||||
|
me_queue_pressure_delta: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PressureState {
|
||||||
|
active: bool,
|
||||||
|
low_streak: u8,
|
||||||
|
prev_accept_timeout_total: u64,
|
||||||
|
prev_me_queue_pressure_total: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PressureState {
|
||||||
|
fn new(stats: &Stats) -> Self {
|
||||||
|
Self {
|
||||||
|
active: false,
|
||||||
|
low_streak: 0,
|
||||||
|
prev_accept_timeout_total: stats.get_accept_permit_timeout_total(),
|
||||||
|
prev_me_queue_pressure_total: stats.get_me_c2me_send_full_total(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spawn_conntrack_controller(
|
||||||
|
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
|
) {
|
||||||
|
if !cfg!(target_os = "linux") {
|
||||||
|
let enabled = config_rx
|
||||||
|
.borrow()
|
||||||
|
.server
|
||||||
|
.conntrack_control
|
||||||
|
.inline_conntrack_control;
|
||||||
|
stats.set_conntrack_control_enabled(enabled);
|
||||||
|
stats.set_conntrack_control_available(false);
|
||||||
|
stats.set_conntrack_pressure_active(false);
|
||||||
|
stats.set_conntrack_event_queue_depth(0);
|
||||||
|
stats.set_conntrack_rule_apply_ok(false);
|
||||||
|
shared.disable_conntrack_close_sender();
|
||||||
|
shared.set_conntrack_pressure_active(false);
|
||||||
|
if enabled {
|
||||||
|
warn!(
|
||||||
|
"conntrack control is configured but unsupported on this OS; disabling runtime worker"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel(CONNTRACK_EVENT_QUEUE_CAPACITY);
|
||||||
|
shared.set_conntrack_close_sender(tx);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
run_conntrack_controller(config_rx, stats, shared, rx).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_conntrack_controller(
|
||||||
|
mut config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
|
mut close_rx: mpsc::Receiver<ConntrackCloseEvent>,
|
||||||
|
) {
|
||||||
|
let mut cfg = config_rx.borrow().clone();
|
||||||
|
let mut pressure_state = PressureState::new(stats.as_ref());
|
||||||
|
let mut delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
|
||||||
|
let mut backend = pick_backend(cfg.server.conntrack_control.backend);
|
||||||
|
|
||||||
|
apply_runtime_state(
|
||||||
|
stats.as_ref(),
|
||||||
|
shared.as_ref(),
|
||||||
|
&cfg,
|
||||||
|
backend.is_some(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
reconcile_rules(&cfg, backend, stats.as_ref()).await;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
changed = config_rx.changed() => {
|
||||||
|
if changed.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cfg = config_rx.borrow_and_update().clone();
|
||||||
|
backend = pick_backend(cfg.server.conntrack_control.backend);
|
||||||
|
delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
|
||||||
|
apply_runtime_state(stats.as_ref(), shared.as_ref(), &cfg, backend.is_some(), pressure_state.active);
|
||||||
|
reconcile_rules(&cfg, backend, stats.as_ref()).await;
|
||||||
|
}
|
||||||
|
event = close_rx.recv() => {
|
||||||
|
let Some(event) = event else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
stats.set_conntrack_event_queue_depth(close_rx.len() as u64);
|
||||||
|
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !pressure_state.active {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !matches!(event.reason, ConntrackCloseReason::Timeout | ConntrackCloseReason::Pressure | ConntrackCloseReason::Reset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if delete_budget_tokens == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stats.increment_conntrack_delete_attempt_total();
|
||||||
|
match delete_conntrack_entry(event).await {
|
||||||
|
DeleteOutcome::Deleted => {
|
||||||
|
delete_budget_tokens = delete_budget_tokens.saturating_sub(1);
|
||||||
|
stats.increment_conntrack_delete_success_total();
|
||||||
|
}
|
||||||
|
DeleteOutcome::NotFound => {
|
||||||
|
delete_budget_tokens = delete_budget_tokens.saturating_sub(1);
|
||||||
|
stats.increment_conntrack_delete_not_found_total();
|
||||||
|
}
|
||||||
|
DeleteOutcome::Error => {
|
||||||
|
delete_budget_tokens = delete_budget_tokens.saturating_sub(1);
|
||||||
|
stats.increment_conntrack_delete_error_total();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(PRESSURE_SAMPLE_INTERVAL) => {
|
||||||
|
delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
|
||||||
|
stats.set_conntrack_event_queue_depth(close_rx.len() as u64);
|
||||||
|
let sample = collect_pressure_sample(stats.as_ref(), &cfg, &mut pressure_state);
|
||||||
|
update_pressure_state(
|
||||||
|
stats.as_ref(),
|
||||||
|
shared.as_ref(),
|
||||||
|
&cfg,
|
||||||
|
&sample,
|
||||||
|
&mut pressure_state,
|
||||||
|
);
|
||||||
|
if pressure_state.active {
|
||||||
|
note_global_relay_pressure(shared.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.disable_conntrack_close_sender();
|
||||||
|
shared.set_conntrack_pressure_active(false);
|
||||||
|
stats.set_conntrack_pressure_active(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_runtime_state(
|
||||||
|
stats: &Stats,
|
||||||
|
shared: &ProxySharedState,
|
||||||
|
cfg: &ProxyConfig,
|
||||||
|
backend_available: bool,
|
||||||
|
pressure_active: bool,
|
||||||
|
) {
|
||||||
|
let enabled = cfg.server.conntrack_control.inline_conntrack_control;
|
||||||
|
let available = enabled && backend_available && has_cap_net_admin();
|
||||||
|
if enabled && !available {
|
||||||
|
warn!(
|
||||||
|
"conntrack control enabled but unavailable (missing CAP_NET_ADMIN or backend binaries)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
stats.set_conntrack_control_enabled(enabled);
|
||||||
|
stats.set_conntrack_control_available(available);
|
||||||
|
shared.set_conntrack_pressure_active(enabled && pressure_active);
|
||||||
|
stats.set_conntrack_pressure_active(enabled && pressure_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_pressure_sample(
|
||||||
|
stats: &Stats,
|
||||||
|
cfg: &ProxyConfig,
|
||||||
|
state: &mut PressureState,
|
||||||
|
) -> PressureSample {
|
||||||
|
let current_connections = stats.get_current_connections_total();
|
||||||
|
let conn_pct = if cfg.server.max_connections == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
((current_connections.saturating_mul(100)) / u64::from(cfg.server.max_connections))
|
||||||
|
.min(100) as u8,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let fd_pct = fd_usage_pct();
|
||||||
|
|
||||||
|
let accept_total = stats.get_accept_permit_timeout_total();
|
||||||
|
let accept_delta = accept_total.saturating_sub(state.prev_accept_timeout_total);
|
||||||
|
state.prev_accept_timeout_total = accept_total;
|
||||||
|
|
||||||
|
let me_total = stats.get_me_c2me_send_full_total();
|
||||||
|
let me_delta = me_total.saturating_sub(state.prev_me_queue_pressure_total);
|
||||||
|
state.prev_me_queue_pressure_total = me_total;
|
||||||
|
|
||||||
|
PressureSample {
|
||||||
|
conn_pct,
|
||||||
|
fd_pct,
|
||||||
|
accept_timeout_delta: accept_delta,
|
||||||
|
me_queue_pressure_delta: me_delta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_pressure_state(
|
||||||
|
stats: &Stats,
|
||||||
|
shared: &ProxySharedState,
|
||||||
|
cfg: &ProxyConfig,
|
||||||
|
sample: &PressureSample,
|
||||||
|
state: &mut PressureState,
|
||||||
|
) {
|
||||||
|
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||||
|
if state.active {
|
||||||
|
state.active = false;
|
||||||
|
state.low_streak = 0;
|
||||||
|
shared.set_conntrack_pressure_active(false);
|
||||||
|
stats.set_conntrack_pressure_active(false);
|
||||||
|
info!("Conntrack pressure mode deactivated (feature disabled)");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let high = cfg.server.conntrack_control.pressure_high_watermark_pct;
|
||||||
|
let low = cfg.server.conntrack_control.pressure_low_watermark_pct;
|
||||||
|
|
||||||
|
let high_hit = sample.conn_pct.is_some_and(|v| v >= high)
|
||||||
|
|| sample.fd_pct.is_some_and(|v| v >= high)
|
||||||
|
|| sample.accept_timeout_delta > 0
|
||||||
|
|| sample.me_queue_pressure_delta > 0;
|
||||||
|
|
||||||
|
let low_clear = sample.conn_pct.is_none_or(|v| v <= low)
|
||||||
|
&& sample.fd_pct.is_none_or(|v| v <= low)
|
||||||
|
&& sample.accept_timeout_delta == 0
|
||||||
|
&& sample.me_queue_pressure_delta == 0;
|
||||||
|
|
||||||
|
if !state.active && high_hit {
|
||||||
|
state.active = true;
|
||||||
|
state.low_streak = 0;
|
||||||
|
shared.set_conntrack_pressure_active(true);
|
||||||
|
stats.set_conntrack_pressure_active(true);
|
||||||
|
info!(
|
||||||
|
conn_pct = ?sample.conn_pct,
|
||||||
|
fd_pct = ?sample.fd_pct,
|
||||||
|
accept_timeout_delta = sample.accept_timeout_delta,
|
||||||
|
me_queue_pressure_delta = sample.me_queue_pressure_delta,
|
||||||
|
"Conntrack pressure mode activated"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.active && low_clear {
|
||||||
|
state.low_streak = state.low_streak.saturating_add(1);
|
||||||
|
if state.low_streak >= PRESSURE_RELEASE_TICKS {
|
||||||
|
state.active = false;
|
||||||
|
state.low_streak = 0;
|
||||||
|
shared.set_conntrack_pressure_active(false);
|
||||||
|
stats.set_conntrack_pressure_active(false);
|
||||||
|
info!("Conntrack pressure mode deactivated");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.low_streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reconcile_rules(cfg: &ProxyConfig, backend: Option<NetfilterBackend>, stats: &Stats) {
|
||||||
|
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||||
|
clear_notrack_rules_all_backends().await;
|
||||||
|
stats.set_conntrack_rule_apply_ok(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_cap_net_admin() {
|
||||||
|
stats.set_conntrack_rule_apply_ok(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(backend) = backend else {
|
||||||
|
stats.set_conntrack_rule_apply_ok(false);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let apply_result = match backend {
|
||||||
|
NetfilterBackend::Nftables => apply_nft_rules(cfg).await,
|
||||||
|
NetfilterBackend::Iptables => apply_iptables_rules(cfg).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = apply_result {
|
||||||
|
warn!(error = %error, "Failed to reconcile conntrack/notrack rules");
|
||||||
|
stats.set_conntrack_rule_apply_ok(false);
|
||||||
|
} else {
|
||||||
|
stats.set_conntrack_rule_apply_ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_backend(configured: ConntrackBackend) -> Option<NetfilterBackend> {
|
||||||
|
match configured {
|
||||||
|
ConntrackBackend::Auto => {
|
||||||
|
if command_exists("nft") {
|
||||||
|
Some(NetfilterBackend::Nftables)
|
||||||
|
} else if command_exists("iptables") {
|
||||||
|
Some(NetfilterBackend::Iptables)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConntrackBackend::Nftables => command_exists("nft").then_some(NetfilterBackend::Nftables),
|
||||||
|
ConntrackBackend::Iptables => {
|
||||||
|
command_exists("iptables").then_some(NetfilterBackend::Iptables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_exists(binary: &str) -> bool {
|
||||||
|
let Some(path_var) = std::env::var_os("PATH") else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
std::env::split_paths(&path_var).any(|dir| {
|
||||||
|
let candidate: PathBuf = dir.join(binary);
|
||||||
|
candidate.exists() && candidate.is_file()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr>>) {
|
||||||
|
let mode = cfg.server.conntrack_control.mode;
|
||||||
|
let mut v4_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
|
||||||
|
let mut v6_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
ConntrackMode::Tracked => {}
|
||||||
|
ConntrackMode::Notrack => {
|
||||||
|
if cfg.server.listeners.is_empty() {
|
||||||
|
if let Some(ipv4) = cfg
|
||||||
|
.server
|
||||||
|
.listen_addr_ipv4
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse::<IpAddr>().ok())
|
||||||
|
{
|
||||||
|
if ipv4.is_unspecified() {
|
||||||
|
v4_targets.insert(None);
|
||||||
|
} else {
|
||||||
|
v4_targets.insert(Some(ipv4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ipv6) = cfg
|
||||||
|
.server
|
||||||
|
.listen_addr_ipv6
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse::<IpAddr>().ok())
|
||||||
|
{
|
||||||
|
if ipv6.is_unspecified() {
|
||||||
|
v6_targets.insert(None);
|
||||||
|
} else {
|
||||||
|
v6_targets.insert(Some(ipv6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for listener in &cfg.server.listeners {
|
||||||
|
if listener.ip.is_ipv4() {
|
||||||
|
if listener.ip.is_unspecified() {
|
||||||
|
v4_targets.insert(None);
|
||||||
|
} else {
|
||||||
|
v4_targets.insert(Some(listener.ip));
|
||||||
|
}
|
||||||
|
} else if listener.ip.is_unspecified() {
|
||||||
|
v6_targets.insert(None);
|
||||||
|
} else {
|
||||||
|
v6_targets.insert(Some(listener.ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConntrackMode::Hybrid => {
|
||||||
|
for ip in &cfg.server.conntrack_control.hybrid_listener_ips {
|
||||||
|
if ip.is_ipv4() {
|
||||||
|
v4_targets.insert(Some(*ip));
|
||||||
|
} else {
|
||||||
|
v6_targets.insert(Some(*ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
v4_targets.into_iter().collect(),
|
||||||
|
v6_targets.into_iter().collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_nft_rules(cfg: &ProxyConfig) -> Result<(), String> {
|
||||||
|
let _ = run_command(
|
||||||
|
"nft",
|
||||||
|
&["delete", "table", "inet", "telemt_conntrack"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if matches!(cfg.server.conntrack_control.mode, ConntrackMode::Tracked) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (v4_targets, v6_targets) = notrack_targets(cfg);
|
||||||
|
let mut rules = Vec::new();
|
||||||
|
for ip in v4_targets {
|
||||||
|
let rule = if let Some(ip) = ip {
|
||||||
|
format!("tcp dport {} ip daddr {} notrack", cfg.server.port, ip)
|
||||||
|
} else {
|
||||||
|
format!("tcp dport {} notrack", cfg.server.port)
|
||||||
|
};
|
||||||
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
for ip in v6_targets {
|
||||||
|
let rule = if let Some(ip) = ip {
|
||||||
|
format!("tcp dport {} ip6 daddr {} notrack", cfg.server.port, ip)
|
||||||
|
} else {
|
||||||
|
format!("tcp dport {} notrack", cfg.server.port)
|
||||||
|
};
|
||||||
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rule_blob = if rules.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" {}\n", rules.join("\n "))
|
||||||
|
};
|
||||||
|
let script = format!(
|
||||||
|
"table inet telemt_conntrack {{\n chain preraw {{\n type filter hook prerouting priority raw; policy accept;\n{rule_blob} }}\n}}\n"
|
||||||
|
);
|
||||||
|
run_command("nft", &["-f", "-"], Some(script)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_iptables_rules(cfg: &ProxyConfig) -> Result<(), String> {
|
||||||
|
apply_iptables_rules_for_binary("iptables", cfg, true).await?;
|
||||||
|
apply_iptables_rules_for_binary("ip6tables", cfg, false).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_iptables_rules_for_binary(
|
||||||
|
binary: &str,
|
||||||
|
cfg: &ProxyConfig,
|
||||||
|
ipv4: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if !command_exists(binary) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let chain = "TELEMT_NOTRACK";
|
||||||
|
let _ = run_command(
|
||||||
|
binary,
|
||||||
|
&["-t", "raw", "-D", "PREROUTING", "-j", chain],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = run_command(binary, &["-t", "raw", "-F", chain], None).await;
|
||||||
|
let _ = run_command(binary, &["-t", "raw", "-X", chain], None).await;
|
||||||
|
|
||||||
|
if matches!(cfg.server.conntrack_control.mode, ConntrackMode::Tracked) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
run_command(binary, &["-t", "raw", "-N", chain], None).await?;
|
||||||
|
run_command(binary, &["-t", "raw", "-F", chain], None).await?;
|
||||||
|
if run_command(
|
||||||
|
binary,
|
||||||
|
&["-t", "raw", "-C", "PREROUTING", "-j", chain],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
run_command(
|
||||||
|
binary,
|
||||||
|
&["-t", "raw", "-I", "PREROUTING", "1", "-j", chain],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (v4_targets, v6_targets) = notrack_targets(cfg);
|
||||||
|
let selected = if ipv4 { v4_targets } else { v6_targets };
|
||||||
|
for ip in selected {
|
||||||
|
let mut args = vec![
|
||||||
|
"-t".to_string(),
|
||||||
|
"raw".to_string(),
|
||||||
|
"-A".to_string(),
|
||||||
|
chain.to_string(),
|
||||||
|
"-p".to_string(),
|
||||||
|
"tcp".to_string(),
|
||||||
|
"--dport".to_string(),
|
||||||
|
cfg.server.port.to_string(),
|
||||||
|
];
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
args.push("-d".to_string());
|
||||||
|
args.push(ip.to_string());
|
||||||
|
}
|
||||||
|
args.push("-j".to_string());
|
||||||
|
args.push("CT".to_string());
|
||||||
|
args.push("--notrack".to_string());
|
||||||
|
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||||
|
run_command(binary, &arg_refs, None).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_notrack_rules_all_backends() {
|
||||||
|
let _ = run_command(
|
||||||
|
"nft",
|
||||||
|
&["delete", "table", "inet", "telemt_conntrack"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = run_command(
|
||||||
|
"iptables",
|
||||||
|
&["-t", "raw", "-D", "PREROUTING", "-j", "TELEMT_NOTRACK"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = run_command("iptables", &["-t", "raw", "-F", "TELEMT_NOTRACK"], None).await;
|
||||||
|
let _ = run_command("iptables", &["-t", "raw", "-X", "TELEMT_NOTRACK"], None).await;
|
||||||
|
let _ = run_command(
|
||||||
|
"ip6tables",
|
||||||
|
&["-t", "raw", "-D", "PREROUTING", "-j", "TELEMT_NOTRACK"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = run_command("ip6tables", &["-t", "raw", "-F", "TELEMT_NOTRACK"], None).await;
|
||||||
|
let _ = run_command("ip6tables", &["-t", "raw", "-X", "TELEMT_NOTRACK"], None).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeleteOutcome {
|
||||||
|
Deleted,
|
||||||
|
NotFound,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_conntrack_entry(event: ConntrackCloseEvent) -> DeleteOutcome {
|
||||||
|
if !command_exists("conntrack") {
|
||||||
|
return DeleteOutcome::Error;
|
||||||
|
}
|
||||||
|
let args = vec![
|
||||||
|
"-D".to_string(),
|
||||||
|
"-p".to_string(),
|
||||||
|
"tcp".to_string(),
|
||||||
|
"-s".to_string(),
|
||||||
|
event.src.ip().to_string(),
|
||||||
|
"--sport".to_string(),
|
||||||
|
event.src.port().to_string(),
|
||||||
|
"-d".to_string(),
|
||||||
|
event.dst.ip().to_string(),
|
||||||
|
"--dport".to_string(),
|
||||||
|
event.dst.port().to_string(),
|
||||||
|
];
|
||||||
|
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||||
|
match run_command("conntrack", &arg_refs, None).await {
|
||||||
|
Ok(()) => DeleteOutcome::Deleted,
|
||||||
|
Err(error) => {
|
||||||
|
if error.contains("0 flow entries have been deleted") {
|
||||||
|
DeleteOutcome::NotFound
|
||||||
|
} else {
|
||||||
|
debug!(error = %error, "conntrack delete failed");
|
||||||
|
DeleteOutcome::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_command(binary: &str, args: &[&str], stdin: Option<String>) -> Result<(), String> {
|
||||||
|
if !command_exists(binary) {
|
||||||
|
return Err(format!("{binary} is not available"));
|
||||||
|
}
|
||||||
|
let mut command = Command::new(binary);
|
||||||
|
command.args(args);
|
||||||
|
if stdin.is_some() {
|
||||||
|
command.stdin(std::process::Stdio::piped());
|
||||||
|
}
|
||||||
|
command.stdout(std::process::Stdio::null());
|
||||||
|
command.stderr(std::process::Stdio::piped());
|
||||||
|
let mut child = command
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("spawn {binary} failed: {e}"))?;
|
||||||
|
if let Some(blob) = stdin
|
||||||
|
&& let Some(mut writer) = child.stdin.take()
|
||||||
|
{
|
||||||
|
writer
|
||||||
|
.write_all(blob.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("stdin write {binary} failed: {e}"))?;
|
||||||
|
}
|
||||||
|
let output = child
|
||||||
|
.wait_with_output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("wait {binary} failed: {e}"))?;
|
||||||
|
if output.status.success() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
Err(if stderr.is_empty() {
|
||||||
|
format!("{binary} exited with status {}", output.status)
|
||||||
|
} else {
|
||||||
|
stderr
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fd_usage_pct() -> Option<u8> {
|
||||||
|
let soft_limit = nofile_soft_limit()?;
|
||||||
|
if soft_limit == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let fd_count = std::fs::read_dir("/proc/self/fd").ok()?.count() as u64;
|
||||||
|
Some(((fd_count.saturating_mul(100)) / soft_limit).min(100) as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nofile_soft_limit() -> Option<u64> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let mut lim = libc::rlimit {
|
||||||
|
rlim_cur: 0,
|
||||||
|
rlim_max: 0,
|
||||||
|
};
|
||||||
|
let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) };
|
||||||
|
if rc != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(lim.rlim_cur);
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_cap_net_admin() -> bool {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let Ok(status) = std::fs::read_to_string("/proc/self/status") else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
for line in status.lines() {
|
||||||
|
if let Some(raw) = line.strip_prefix("CapEff:") {
|
||||||
|
let caps = raw.trim();
|
||||||
|
if let Ok(bits) = u64::from_str_radix(caps, 16) {
|
||||||
|
const CAP_NET_ADMIN_BIT: u64 = 12;
|
||||||
|
return (bits & (1u64 << CAP_NET_ADMIN_BIT)) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::config::ProxyConfig;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressure_activates_on_accept_timeout_spike() {
|
||||||
|
let stats = Stats::new();
|
||||||
|
let shared = ProxySharedState::new();
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.server.conntrack_control.inline_conntrack_control = true;
|
||||||
|
let mut state = PressureState::new(&stats);
|
||||||
|
let sample = PressureSample {
|
||||||
|
conn_pct: Some(10),
|
||||||
|
fd_pct: Some(10),
|
||||||
|
accept_timeout_delta: 1,
|
||||||
|
me_queue_pressure_delta: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &sample, &mut state);
|
||||||
|
|
||||||
|
assert!(state.active);
|
||||||
|
assert!(shared.conntrack_pressure_active());
|
||||||
|
assert!(stats.get_conntrack_pressure_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressure_releases_after_hysteresis_window() {
|
||||||
|
let stats = Stats::new();
|
||||||
|
let shared = ProxySharedState::new();
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.server.conntrack_control.inline_conntrack_control = true;
|
||||||
|
let mut state = PressureState::new(&stats);
|
||||||
|
|
||||||
|
let high_sample = PressureSample {
|
||||||
|
conn_pct: Some(95),
|
||||||
|
fd_pct: Some(95),
|
||||||
|
accept_timeout_delta: 0,
|
||||||
|
me_queue_pressure_delta: 0,
|
||||||
|
};
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &high_sample, &mut state);
|
||||||
|
assert!(state.active);
|
||||||
|
|
||||||
|
let low_sample = PressureSample {
|
||||||
|
conn_pct: Some(10),
|
||||||
|
fd_pct: Some(10),
|
||||||
|
accept_timeout_delta: 0,
|
||||||
|
me_queue_pressure_delta: 0,
|
||||||
|
};
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||||
|
assert!(state.active);
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||||
|
assert!(state.active);
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||||
|
|
||||||
|
assert!(!state.active);
|
||||||
|
assert!(!shared.conntrack_pressure_active());
|
||||||
|
assert!(!stats.get_conntrack_pressure_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressure_does_not_activate_when_disabled() {
|
||||||
|
let stats = Stats::new();
|
||||||
|
let shared = ProxySharedState::new();
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.server.conntrack_control.inline_conntrack_control = false;
|
||||||
|
let mut state = PressureState::new(&stats);
|
||||||
|
let sample = PressureSample {
|
||||||
|
conn_pct: Some(100),
|
||||||
|
fd_pct: Some(100),
|
||||||
|
accept_timeout_delta: 10,
|
||||||
|
me_queue_pressure_delta: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &sample, &mut state);
|
||||||
|
|
||||||
|
assert!(!state.active);
|
||||||
|
assert!(!shared.conntrack_pressure_active());
|
||||||
|
assert!(!stats.get_conntrack_pressure_active());
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-19
@@ -13,10 +13,13 @@
|
|||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use aes::Aes256;
|
|
||||||
use ctr::{Ctr128BE, cipher::{KeyIvInit, StreamCipher}};
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
use crate::error::{ProxyError, Result};
|
use crate::error::{ProxyError, Result};
|
||||||
|
use aes::Aes256;
|
||||||
|
use ctr::{
|
||||||
|
Ctr128BE,
|
||||||
|
cipher::{KeyIvInit, StreamCipher},
|
||||||
|
};
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
type Aes256Ctr = Ctr128BE<Aes256>;
|
type Aes256Ctr = Ctr128BE<Aes256>;
|
||||||
|
|
||||||
@@ -46,10 +49,16 @@ impl AesCtr {
|
|||||||
/// Create from key and IV slices
|
/// Create from key and IV slices
|
||||||
pub fn from_key_iv(key: &[u8], iv: &[u8]) -> Result<Self> {
|
pub fn from_key_iv(key: &[u8], iv: &[u8]) -> Result<Self> {
|
||||||
if key.len() != 32 {
|
if key.len() != 32 {
|
||||||
return Err(ProxyError::InvalidKeyLength { expected: 32, got: key.len() });
|
return Err(ProxyError::InvalidKeyLength {
|
||||||
|
expected: 32,
|
||||||
|
got: key.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if iv.len() != 16 {
|
if iv.len() != 16 {
|
||||||
return Err(ProxyError::InvalidKeyLength { expected: 16, got: iv.len() });
|
return Err(ProxyError::InvalidKeyLength {
|
||||||
|
expected: 16,
|
||||||
|
got: iv.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let key: [u8; 32] = key.try_into().unwrap();
|
let key: [u8; 32] = key.try_into().unwrap();
|
||||||
@@ -108,10 +117,16 @@ impl AesCbc {
|
|||||||
/// Create from slices
|
/// Create from slices
|
||||||
pub fn from_slices(key: &[u8], iv: &[u8]) -> Result<Self> {
|
pub fn from_slices(key: &[u8], iv: &[u8]) -> Result<Self> {
|
||||||
if key.len() != 32 {
|
if key.len() != 32 {
|
||||||
return Err(ProxyError::InvalidKeyLength { expected: 32, got: key.len() });
|
return Err(ProxyError::InvalidKeyLength {
|
||||||
|
expected: 32,
|
||||||
|
got: key.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if iv.len() != 16 {
|
if iv.len() != 16 {
|
||||||
return Err(ProxyError::InvalidKeyLength { expected: 16, got: iv.len() });
|
return Err(ProxyError::InvalidKeyLength {
|
||||||
|
expected: 16,
|
||||||
|
got: iv.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -150,9 +165,10 @@ impl AesCbc {
|
|||||||
/// CBC Encryption: C[i] = AES_Encrypt(P[i] XOR C[i-1]), where C[-1] = IV
|
/// CBC Encryption: C[i] = AES_Encrypt(P[i] XOR C[i-1]), where C[-1] = IV
|
||||||
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||||
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
return Err(ProxyError::Crypto(
|
return Err(ProxyError::Crypto(format!(
|
||||||
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
|
"CBC data must be aligned to 16 bytes, got {}",
|
||||||
));
|
data.len()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
@@ -181,9 +197,10 @@ impl AesCbc {
|
|||||||
/// CBC Decryption: P[i] = AES_Decrypt(C[i]) XOR C[i-1], where C[-1] = IV
|
/// CBC Decryption: P[i] = AES_Decrypt(C[i]) XOR C[i-1], where C[-1] = IV
|
||||||
pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||||
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
return Err(ProxyError::Crypto(
|
return Err(ProxyError::Crypto(format!(
|
||||||
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
|
"CBC data must be aligned to 16 bytes, got {}",
|
||||||
));
|
data.len()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
@@ -210,9 +227,10 @@ impl AesCbc {
|
|||||||
/// Encrypt data in-place
|
/// Encrypt data in-place
|
||||||
pub fn encrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
|
pub fn encrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
|
||||||
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
return Err(ProxyError::Crypto(
|
return Err(ProxyError::Crypto(format!(
|
||||||
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
|
"CBC data must be aligned to 16 bytes, got {}",
|
||||||
));
|
data.len()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
@@ -243,9 +261,10 @@ impl AesCbc {
|
|||||||
/// Decrypt data in-place
|
/// Decrypt data in-place
|
||||||
pub fn decrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
|
pub fn decrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
|
||||||
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
return Err(ProxyError::Crypto(
|
return Err(ProxyError::Crypto(format!(
|
||||||
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
|
"CBC data must be aligned to 16 bytes, got {}",
|
||||||
));
|
data.len()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
|
|||||||
+6
-25
@@ -12,10 +12,10 @@
|
|||||||
//! usages are intentional and protocol-mandated.
|
//! usages are intentional and protocol-mandated.
|
||||||
|
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use sha2::Sha256;
|
|
||||||
use md5::Md5;
|
use md5::Md5;
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
@@ -28,8 +28,7 @@ pub fn sha256(data: &[u8]) -> [u8; 32] {
|
|||||||
|
|
||||||
/// SHA-256 HMAC
|
/// SHA-256 HMAC
|
||||||
pub fn sha256_hmac(key: &[u8], data: &[u8]) -> [u8; 32] {
|
pub fn sha256_hmac(key: &[u8], data: &[u8]) -> [u8; 32] {
|
||||||
let mut mac = HmacSha256::new_from_slice(key)
|
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
|
||||||
.expect("HMAC accepts any key length");
|
|
||||||
mac.update(data);
|
mac.update(data);
|
||||||
mac.finalize().into_bytes().into()
|
mac.finalize().into_bytes().into()
|
||||||
}
|
}
|
||||||
@@ -124,17 +123,8 @@ pub fn derive_middleproxy_keys(
|
|||||||
srv_ipv6: Option<&[u8; 16]>,
|
srv_ipv6: Option<&[u8; 16]>,
|
||||||
) -> ([u8; 32], [u8; 16]) {
|
) -> ([u8; 32], [u8; 16]) {
|
||||||
let s = build_middleproxy_prekey(
|
let s = build_middleproxy_prekey(
|
||||||
nonce_srv,
|
nonce_srv, nonce_clt, clt_ts, srv_ip, clt_port, purpose, clt_ip, srv_port, secret,
|
||||||
nonce_clt,
|
clt_ipv6, srv_ipv6,
|
||||||
clt_ts,
|
|
||||||
srv_ip,
|
|
||||||
clt_port,
|
|
||||||
purpose,
|
|
||||||
clt_ip,
|
|
||||||
srv_port,
|
|
||||||
secret,
|
|
||||||
clt_ipv6,
|
|
||||||
srv_ipv6,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let md5_1 = md5(&s[1..]);
|
let md5_1 = md5(&s[1..]);
|
||||||
@@ -164,17 +154,8 @@ mod tests {
|
|||||||
let secret = vec![0x55u8; 128];
|
let secret = vec![0x55u8; 128];
|
||||||
|
|
||||||
let prekey = build_middleproxy_prekey(
|
let prekey = build_middleproxy_prekey(
|
||||||
&nonce_srv,
|
&nonce_srv, &nonce_clt, &clt_ts, srv_ip, &clt_port, b"CLIENT", clt_ip, &srv_port,
|
||||||
&nonce_clt,
|
&secret, None, None,
|
||||||
&clt_ts,
|
|
||||||
srv_ip,
|
|
||||||
&clt_port,
|
|
||||||
b"CLIENT",
|
|
||||||
clt_ip,
|
|
||||||
&srv_port,
|
|
||||||
&secret,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
let digest = sha256(&prekey);
|
let digest = sha256(&prekey);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ pub mod aes;
|
|||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod random;
|
pub mod random;
|
||||||
|
|
||||||
pub use aes::{AesCtr, AesCbc};
|
pub use aes::{AesCbc, AesCtr};
|
||||||
pub use hash::{
|
pub use hash::{
|
||||||
build_middleproxy_prekey, crc32, crc32c, derive_middleproxy_keys, sha256, sha256_hmac,
|
build_middleproxy_prekey, crc32, crc32c, derive_middleproxy_keys, sha256, sha256_hmac,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
#![allow(deprecated)]
|
#![allow(deprecated)]
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use rand::{Rng, RngCore, SeedableRng};
|
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
use crate::crypto::AesCtr;
|
use crate::crypto::AesCtr;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
use rand::{Rng, RngExt, SeedableRng};
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
/// Cryptographically secure PRNG with AES-CTR
|
/// Cryptographically secure PRNG with AES-CTR
|
||||||
pub struct SecureRandom {
|
pub struct SecureRandom {
|
||||||
@@ -101,7 +101,7 @@ impl SecureRandom {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
inner.rng.gen_range(0..max)
|
inner.rng.random_range(0..max)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate random bits
|
/// Generate random bits
|
||||||
@@ -141,7 +141,7 @@ impl SecureRandom {
|
|||||||
pub fn shuffle<T>(&self, slice: &mut [T]) {
|
pub fn shuffle<T>(&self, slice: &mut [T]) {
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
for i in (1..slice.len()).rev() {
|
for i in (1..slice.len()).rev() {
|
||||||
let j = inner.rng.gen_range(0..=i);
|
let j = inner.rng.random_range(0..=i);
|
||||||
slice.swap(i, j);
|
slice.swap(i, j);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,541 @@
|
|||||||
|
//! 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-47
@@ -12,28 +12,15 @@ use thiserror::Error;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum StreamError {
|
pub enum StreamError {
|
||||||
/// Partial read: got fewer bytes than expected
|
/// Partial read: got fewer bytes than expected
|
||||||
PartialRead {
|
PartialRead { expected: usize, got: usize },
|
||||||
expected: usize,
|
|
||||||
got: usize,
|
|
||||||
},
|
|
||||||
/// Partial write: wrote fewer bytes than expected
|
/// Partial write: wrote fewer bytes than expected
|
||||||
PartialWrite {
|
PartialWrite { expected: usize, written: usize },
|
||||||
expected: usize,
|
|
||||||
written: usize,
|
|
||||||
},
|
|
||||||
/// Stream is in poisoned state and cannot be used
|
/// Stream is in poisoned state and cannot be used
|
||||||
Poisoned {
|
Poisoned { reason: String },
|
||||||
reason: String,
|
|
||||||
},
|
|
||||||
/// Buffer overflow: attempted to buffer more than allowed
|
/// Buffer overflow: attempted to buffer more than allowed
|
||||||
BufferOverflow {
|
BufferOverflow { limit: usize, attempted: usize },
|
||||||
limit: usize,
|
|
||||||
attempted: usize,
|
|
||||||
},
|
|
||||||
/// Invalid frame format
|
/// Invalid frame format
|
||||||
InvalidFrame {
|
InvalidFrame { details: String },
|
||||||
details: String,
|
|
||||||
},
|
|
||||||
/// Unexpected end of stream
|
/// Unexpected end of stream
|
||||||
UnexpectedEof,
|
UnexpectedEof,
|
||||||
/// Underlying I/O error
|
/// Underlying I/O error
|
||||||
@@ -47,13 +34,21 @@ impl fmt::Display for StreamError {
|
|||||||
write!(f, "partial read: expected {} bytes, got {}", expected, got)
|
write!(f, "partial read: expected {} bytes, got {}", expected, got)
|
||||||
}
|
}
|
||||||
Self::PartialWrite { expected, written } => {
|
Self::PartialWrite { expected, written } => {
|
||||||
write!(f, "partial write: expected {} bytes, wrote {}", expected, written)
|
write!(
|
||||||
|
f,
|
||||||
|
"partial write: expected {} bytes, wrote {}",
|
||||||
|
expected, written
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Self::Poisoned { reason } => {
|
Self::Poisoned { reason } => {
|
||||||
write!(f, "stream poisoned: {}", reason)
|
write!(f, "stream poisoned: {}", reason)
|
||||||
}
|
}
|
||||||
Self::BufferOverflow { limit, attempted } => {
|
Self::BufferOverflow { limit, attempted } => {
|
||||||
write!(f, "buffer overflow: limit {}, attempted {}", limit, attempted)
|
write!(
|
||||||
|
f,
|
||||||
|
"buffer overflow: limit {}, attempted {}",
|
||||||
|
limit, attempted
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Self::InvalidFrame { details } => {
|
Self::InvalidFrame { details } => {
|
||||||
write!(f, "invalid frame: {}", details)
|
write!(f, "invalid frame: {}", details)
|
||||||
@@ -90,9 +85,7 @@ impl From<StreamError> for std::io::Error {
|
|||||||
StreamError::UnexpectedEof => {
|
StreamError::UnexpectedEof => {
|
||||||
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, err)
|
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, err)
|
||||||
}
|
}
|
||||||
StreamError::Poisoned { .. } => {
|
StreamError::Poisoned { .. } => std::io::Error::other(err),
|
||||||
std::io::Error::other(err)
|
|
||||||
}
|
|
||||||
StreamError::BufferOverflow { .. } => {
|
StreamError::BufferOverflow { .. } => {
|
||||||
std::io::Error::new(std::io::ErrorKind::OutOfMemory, err)
|
std::io::Error::new(std::io::ErrorKind::OutOfMemory, err)
|
||||||
}
|
}
|
||||||
@@ -124,8 +117,8 @@ impl Recoverable for StreamError {
|
|||||||
Self::Io(e) => matches!(
|
Self::Io(e) => matches!(
|
||||||
e.kind(),
|
e.kind(),
|
||||||
std::io::ErrorKind::WouldBlock
|
std::io::ErrorKind::WouldBlock
|
||||||
| std::io::ErrorKind::Interrupted
|
| std::io::ErrorKind::Interrupted
|
||||||
| std::io::ErrorKind::TimedOut
|
| std::io::ErrorKind::TimedOut
|
||||||
),
|
),
|
||||||
Self::Poisoned { .. }
|
Self::Poisoned { .. }
|
||||||
| Self::BufferOverflow { .. }
|
| Self::BufferOverflow { .. }
|
||||||
@@ -135,7 +128,10 @@ impl Recoverable for StreamError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn can_continue(&self) -> bool {
|
fn can_continue(&self) -> bool {
|
||||||
!matches!(self, Self::Poisoned { .. } | Self::UnexpectedEof | Self::BufferOverflow { .. })
|
!matches!(
|
||||||
|
self,
|
||||||
|
Self::Poisoned { .. } | Self::UnexpectedEof | Self::BufferOverflow { .. }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +140,8 @@ impl Recoverable for std::io::Error {
|
|||||||
matches!(
|
matches!(
|
||||||
self.kind(),
|
self.kind(),
|
||||||
std::io::ErrorKind::WouldBlock
|
std::io::ErrorKind::WouldBlock
|
||||||
| std::io::ErrorKind::Interrupted
|
| std::io::ErrorKind::Interrupted
|
||||||
| std::io::ErrorKind::TimedOut
|
| std::io::ErrorKind::TimedOut
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,9 +149,9 @@ impl Recoverable for std::io::Error {
|
|||||||
!matches!(
|
!matches!(
|
||||||
self.kind(),
|
self.kind(),
|
||||||
std::io::ErrorKind::BrokenPipe
|
std::io::ErrorKind::BrokenPipe
|
||||||
| std::io::ErrorKind::ConnectionReset
|
| std::io::ErrorKind::ConnectionReset
|
||||||
| std::io::ErrorKind::ConnectionAborted
|
| std::io::ErrorKind::ConnectionAborted
|
||||||
| std::io::ErrorKind::NotConnected
|
| std::io::ErrorKind::NotConnected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +161,6 @@ impl Recoverable for std::io::Error {
|
|||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ProxyError {
|
pub enum ProxyError {
|
||||||
// ============= Crypto Errors =============
|
// ============= Crypto Errors =============
|
||||||
|
|
||||||
#[error("Crypto error: {0}")]
|
#[error("Crypto error: {0}")]
|
||||||
Crypto(String),
|
Crypto(String),
|
||||||
|
|
||||||
@@ -173,12 +168,10 @@ pub enum ProxyError {
|
|||||||
InvalidKeyLength { expected: usize, got: usize },
|
InvalidKeyLength { expected: usize, got: usize },
|
||||||
|
|
||||||
// ============= Stream Errors =============
|
// ============= Stream Errors =============
|
||||||
|
|
||||||
#[error("Stream error: {0}")]
|
#[error("Stream error: {0}")]
|
||||||
Stream(#[from] StreamError),
|
Stream(#[from] StreamError),
|
||||||
|
|
||||||
// ============= Protocol Errors =============
|
// ============= Protocol Errors =============
|
||||||
|
|
||||||
#[error("Invalid handshake: {0}")]
|
#[error("Invalid handshake: {0}")]
|
||||||
InvalidHandshake(String),
|
InvalidHandshake(String),
|
||||||
|
|
||||||
@@ -210,7 +203,6 @@ pub enum ProxyError {
|
|||||||
TgHandshakeTimeout,
|
TgHandshakeTimeout,
|
||||||
|
|
||||||
// ============= Network Errors =============
|
// ============= Network Errors =============
|
||||||
|
|
||||||
#[error("Connection timeout to {addr}")]
|
#[error("Connection timeout to {addr}")]
|
||||||
ConnectionTimeout { addr: String },
|
ConnectionTimeout { addr: String },
|
||||||
|
|
||||||
@@ -221,15 +213,16 @@ pub enum ProxyError {
|
|||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
// ============= Proxy Protocol Errors =============
|
// ============= Proxy Protocol Errors =============
|
||||||
|
|
||||||
#[error("Invalid proxy protocol header")]
|
#[error("Invalid proxy protocol header")]
|
||||||
InvalidProxyProtocol,
|
InvalidProxyProtocol,
|
||||||
|
|
||||||
|
#[error("Unknown TLS SNI")]
|
||||||
|
UnknownTlsSni,
|
||||||
|
|
||||||
#[error("Proxy error: {0}")]
|
#[error("Proxy error: {0}")]
|
||||||
Proxy(String),
|
Proxy(String),
|
||||||
|
|
||||||
// ============= Config Errors =============
|
// ============= Config Errors =============
|
||||||
|
|
||||||
#[error("Config error: {0}")]
|
#[error("Config error: {0}")]
|
||||||
Config(String),
|
Config(String),
|
||||||
|
|
||||||
@@ -237,7 +230,6 @@ pub enum ProxyError {
|
|||||||
InvalidSecret { user: String, reason: String },
|
InvalidSecret { user: String, reason: String },
|
||||||
|
|
||||||
// ============= User Errors =============
|
// ============= User Errors =============
|
||||||
|
|
||||||
#[error("User {user} expired")]
|
#[error("User {user} expired")]
|
||||||
UserExpired { user: String },
|
UserExpired { user: String },
|
||||||
|
|
||||||
@@ -254,7 +246,6 @@ pub enum ProxyError {
|
|||||||
RateLimited,
|
RateLimited,
|
||||||
|
|
||||||
// ============= General Errors =============
|
// ============= General Errors =============
|
||||||
|
|
||||||
#[error("Internal error: {0}")]
|
#[error("Internal error: {0}")]
|
||||||
Internal(String),
|
Internal(String),
|
||||||
}
|
}
|
||||||
@@ -311,7 +302,9 @@ impl<T, R, W> HandshakeResult<T, R, W> {
|
|||||||
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> HandshakeResult<U, R, W> {
|
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> HandshakeResult<U, R, W> {
|
||||||
match self {
|
match self {
|
||||||
HandshakeResult::Success(v) => HandshakeResult::Success(f(v)),
|
HandshakeResult::Success(v) => HandshakeResult::Success(f(v)),
|
||||||
HandshakeResult::BadClient { reader, writer } => HandshakeResult::BadClient { reader, writer },
|
HandshakeResult::BadClient { reader, writer } => {
|
||||||
|
HandshakeResult::BadClient { reader, writer }
|
||||||
|
}
|
||||||
HandshakeResult::Error(e) => HandshakeResult::Error(e),
|
HandshakeResult::Error(e) => HandshakeResult::Error(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,18 +334,35 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stream_error_display() {
|
fn test_stream_error_display() {
|
||||||
let err = StreamError::PartialRead { expected: 100, got: 50 };
|
let err = StreamError::PartialRead {
|
||||||
|
expected: 100,
|
||||||
|
got: 50,
|
||||||
|
};
|
||||||
assert!(err.to_string().contains("100"));
|
assert!(err.to_string().contains("100"));
|
||||||
assert!(err.to_string().contains("50"));
|
assert!(err.to_string().contains("50"));
|
||||||
|
|
||||||
let err = StreamError::Poisoned { reason: "test".into() };
|
let err = StreamError::Poisoned {
|
||||||
|
reason: "test".into(),
|
||||||
|
};
|
||||||
assert!(err.to_string().contains("test"));
|
assert!(err.to_string().contains("test"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stream_error_recoverable() {
|
fn test_stream_error_recoverable() {
|
||||||
assert!(StreamError::PartialRead { expected: 10, got: 5 }.is_recoverable());
|
assert!(
|
||||||
assert!(StreamError::PartialWrite { expected: 10, written: 5 }.is_recoverable());
|
StreamError::PartialRead {
|
||||||
|
expected: 10,
|
||||||
|
got: 5
|
||||||
|
}
|
||||||
|
.is_recoverable()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
StreamError::PartialWrite {
|
||||||
|
expected: 10,
|
||||||
|
written: 5
|
||||||
|
}
|
||||||
|
.is_recoverable()
|
||||||
|
);
|
||||||
assert!(!StreamError::Poisoned { reason: "x".into() }.is_recoverable());
|
assert!(!StreamError::Poisoned { reason: "x".into() }.is_recoverable());
|
||||||
assert!(!StreamError::UnexpectedEof.is_recoverable());
|
assert!(!StreamError::UnexpectedEof.is_recoverable());
|
||||||
}
|
}
|
||||||
@@ -361,7 +371,13 @@ mod tests {
|
|||||||
fn test_stream_error_can_continue() {
|
fn test_stream_error_can_continue() {
|
||||||
assert!(!StreamError::Poisoned { reason: "x".into() }.can_continue());
|
assert!(!StreamError::Poisoned { reason: "x".into() }.can_continue());
|
||||||
assert!(!StreamError::UnexpectedEof.can_continue());
|
assert!(!StreamError::UnexpectedEof.can_continue());
|
||||||
assert!(StreamError::PartialRead { expected: 10, got: 5 }.can_continue());
|
assert!(
|
||||||
|
StreamError::PartialRead {
|
||||||
|
expected: 10,
|
||||||
|
got: 5
|
||||||
|
}
|
||||||
|
.can_continue()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -377,7 +393,10 @@ mod tests {
|
|||||||
assert!(success.is_success());
|
assert!(success.is_success());
|
||||||
assert!(!success.is_bad_client());
|
assert!(!success.is_bad_client());
|
||||||
|
|
||||||
let bad: HandshakeResult<i32, (), ()> = HandshakeResult::BadClient { reader: (), writer: () };
|
let bad: HandshakeResult<i32, (), ()> = HandshakeResult::BadClient {
|
||||||
|
reader: (),
|
||||||
|
writer: (),
|
||||||
|
};
|
||||||
assert!(!bad.is_success());
|
assert!(!bad.is_success());
|
||||||
assert!(bad.is_bad_client());
|
assert!(bad.is_bad_client());
|
||||||
}
|
}
|
||||||
@@ -404,7 +423,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_error_display() {
|
fn test_error_display() {
|
||||||
let err = ProxyError::ConnectionTimeout { addr: "1.2.3.4:443".into() };
|
let err = ProxyError::ConnectionTimeout {
|
||||||
|
addr: "1.2.3.4:443".into(),
|
||||||
|
};
|
||||||
assert!(err.to_string().contains("1.2.3.4:443"));
|
assert!(err.to_string().contains("1.2.3.4:443"));
|
||||||
|
|
||||||
let err = ProxyError::InvalidProxyProtocol;
|
let err = ProxyError::InvalidProxyProtocol;
|
||||||
|
|||||||
+112
-6
@@ -5,9 +5,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use tokio::sync::{Mutex as AsyncMutex, RwLock};
|
use tokio::sync::{Mutex as AsyncMutex, RwLock};
|
||||||
|
|
||||||
@@ -22,10 +22,19 @@ pub struct UserIpTracker {
|
|||||||
limit_mode: Arc<RwLock<UserMaxUniqueIpsMode>>,
|
limit_mode: Arc<RwLock<UserMaxUniqueIpsMode>>,
|
||||||
limit_window: Arc<RwLock<Duration>>,
|
limit_window: Arc<RwLock<Duration>>,
|
||||||
last_compact_epoch_secs: Arc<AtomicU64>,
|
last_compact_epoch_secs: Arc<AtomicU64>,
|
||||||
pub(crate) cleanup_queue: Arc<Mutex<Vec<(String, IpAddr)>>>,
|
cleanup_queue: Arc<Mutex<Vec<(String, IpAddr)>>>,
|
||||||
cleanup_drain_lock: Arc<AsyncMutex<()>>,
|
cleanup_drain_lock: Arc<AsyncMutex<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct UserIpTrackerMemoryStats {
|
||||||
|
pub active_users: usize,
|
||||||
|
pub recent_users: usize,
|
||||||
|
pub active_entries: usize,
|
||||||
|
pub recent_entries: usize,
|
||||||
|
pub cleanup_queue_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl UserIpTracker {
|
impl UserIpTracker {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -41,7 +50,6 @@ impl UserIpTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn enqueue_cleanup(&self, user: String, ip: IpAddr) {
|
pub fn enqueue_cleanup(&self, user: String, ip: IpAddr) {
|
||||||
match self.cleanup_queue.lock() {
|
match self.cleanup_queue.lock() {
|
||||||
Ok(mut queue) => queue.push((user, ip)),
|
Ok(mut queue) => queue.push((user, ip)),
|
||||||
@@ -58,6 +66,19 @@ impl UserIpTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn cleanup_queue_len_for_tests(&self) -> usize {
|
||||||
|
self.cleanup_queue
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn cleanup_queue_mutex_for_tests(&self) -> Arc<Mutex<Vec<(String, IpAddr)>>> {
|
||||||
|
Arc::clone(&self.cleanup_queue)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn drain_cleanup_queue(&self) {
|
pub(crate) async fn drain_cleanup_queue(&self) {
|
||||||
// Serialize queue draining and active-IP mutation so check-and-add cannot
|
// Serialize queue draining and active-IP mutation so check-and-add cannot
|
||||||
// observe stale active entries that are already queued for removal.
|
// observe stale active entries that are already queued for removal.
|
||||||
@@ -129,7 +150,15 @@ impl UserIpTracker {
|
|||||||
|
|
||||||
let mut active_ips = self.active_ips.write().await;
|
let mut active_ips = self.active_ips.write().await;
|
||||||
let mut recent_ips = self.recent_ips.write().await;
|
let mut recent_ips = self.recent_ips.write().await;
|
||||||
let mut users = Vec::<String>::with_capacity(active_ips.len().saturating_add(recent_ips.len()));
|
let window = *self.limit_window.read().await;
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
for user_recent in recent_ips.values_mut() {
|
||||||
|
Self::prune_recent(user_recent, now, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut users =
|
||||||
|
Vec::<String>::with_capacity(active_ips.len().saturating_add(recent_ips.len()));
|
||||||
users.extend(active_ips.keys().cloned());
|
users.extend(active_ips.keys().cloned());
|
||||||
for user in recent_ips.keys() {
|
for user in recent_ips.keys() {
|
||||||
if !active_ips.contains_key(user) {
|
if !active_ips.contains_key(user) {
|
||||||
@@ -138,8 +167,14 @@ impl UserIpTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for user in users {
|
for user in users {
|
||||||
let active_empty = active_ips.get(&user).map(|ips| ips.is_empty()).unwrap_or(true);
|
let active_empty = active_ips
|
||||||
let recent_empty = recent_ips.get(&user).map(|ips| ips.is_empty()).unwrap_or(true);
|
.get(&user)
|
||||||
|
.map(|ips| ips.is_empty())
|
||||||
|
.unwrap_or(true);
|
||||||
|
let recent_empty = recent_ips
|
||||||
|
.get(&user)
|
||||||
|
.map(|ips| ips.is_empty())
|
||||||
|
.unwrap_or(true);
|
||||||
if active_empty && recent_empty {
|
if active_empty && recent_empty {
|
||||||
active_ips.remove(&user);
|
active_ips.remove(&user);
|
||||||
recent_ips.remove(&user);
|
recent_ips.remove(&user);
|
||||||
@@ -147,6 +182,26 @@ impl UserIpTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn memory_stats(&self) -> UserIpTrackerMemoryStats {
|
||||||
|
let cleanup_queue_len = self
|
||||||
|
.cleanup_queue
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.len();
|
||||||
|
let active_ips = self.active_ips.read().await;
|
||||||
|
let recent_ips = self.recent_ips.read().await;
|
||||||
|
let active_entries = active_ips.values().map(HashMap::len).sum();
|
||||||
|
let recent_entries = recent_ips.values().map(HashMap::len).sum();
|
||||||
|
|
||||||
|
UserIpTrackerMemoryStats {
|
||||||
|
active_users: active_ips.len(),
|
||||||
|
recent_users: recent_ips.len(),
|
||||||
|
active_entries,
|
||||||
|
recent_entries,
|
||||||
|
cleanup_queue_len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn set_limit_policy(&self, mode: UserMaxUniqueIpsMode, window_secs: u64) {
|
pub async fn set_limit_policy(&self, mode: UserMaxUniqueIpsMode, window_secs: u64) {
|
||||||
{
|
{
|
||||||
let mut current_mode = self.limit_mode.write().await;
|
let mut current_mode = self.limit_mode.write().await;
|
||||||
@@ -432,6 +487,7 @@ impl Default for UserIpTracker {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
fn test_ipv4(oct1: u8, oct2: u8, oct3: u8, oct4: u8) -> IpAddr {
|
fn test_ipv4(oct1: u8, oct2: u8, oct3: u8, oct4: u8) -> IpAddr {
|
||||||
IpAddr::V4(Ipv4Addr::new(oct1, oct2, oct3, oct4))
|
IpAddr::V4(Ipv4Addr::new(oct1, oct2, oct3, oct4))
|
||||||
@@ -745,4 +801,54 @@ mod tests {
|
|||||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||||
assert!(tracker.check_and_add("test_user", ip2).await.is_ok());
|
assert!(tracker.check_and_add("test_user", ip2).await.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_memory_stats_reports_queue_and_entry_counts() {
|
||||||
|
let tracker = UserIpTracker::new();
|
||||||
|
tracker.set_user_limit("test_user", 4).await;
|
||||||
|
let ip1 = test_ipv4(10, 2, 0, 1);
|
||||||
|
let ip2 = test_ipv4(10, 2, 0, 2);
|
||||||
|
|
||||||
|
tracker.check_and_add("test_user", ip1).await.unwrap();
|
||||||
|
tracker.check_and_add("test_user", ip2).await.unwrap();
|
||||||
|
tracker.enqueue_cleanup("test_user".to_string(), ip1);
|
||||||
|
|
||||||
|
let snapshot = tracker.memory_stats().await;
|
||||||
|
assert_eq!(snapshot.active_users, 1);
|
||||||
|
assert_eq!(snapshot.recent_users, 1);
|
||||||
|
assert_eq!(snapshot.active_entries, 2);
|
||||||
|
assert_eq!(snapshot.recent_entries, 2);
|
||||||
|
assert_eq!(snapshot.cleanup_queue_len, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_compact_prunes_stale_recent_entries() {
|
||||||
|
let tracker = UserIpTracker::new();
|
||||||
|
tracker
|
||||||
|
.set_limit_policy(UserMaxUniqueIpsMode::TimeWindow, 1)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let stale_user = "stale-user".to_string();
|
||||||
|
let stale_ip = test_ipv4(10, 3, 0, 1);
|
||||||
|
{
|
||||||
|
let mut recent_ips = tracker.recent_ips.write().await;
|
||||||
|
recent_ips
|
||||||
|
.entry(stale_user.clone())
|
||||||
|
.or_insert_with(HashMap::new)
|
||||||
|
.insert(stale_ip, Instant::now() - Duration::from_secs(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.last_compact_epoch_secs.store(0, Ordering::Relaxed);
|
||||||
|
tracker
|
||||||
|
.check_and_add("trigger-user", test_ipv4(10, 3, 0, 2))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let recent_ips = tracker.recent_ips.read().await;
|
||||||
|
let stale_exists = recent_ips
|
||||||
|
.get(&stale_user)
|
||||||
|
.map(|ips| ips.contains_key(&stale_ip))
|
||||||
|
.unwrap_or(false);
|
||||||
|
assert!(!stale_exists);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+343
@@ -0,0 +1,343 @@
|
|||||||
|
//! 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(false)
|
||||||
|
.with_level(false)
|
||||||
|
.without_time()
|
||||||
|
.with_writer(SyslogMakeWriter::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)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct SyslogMakeWriter;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct SyslogWriter {
|
||||||
|
priority: libc::c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
impl SyslogMakeWriter {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn syslog_priority_for_level(level: &tracing::Level) -> libc::c_int {
|
||||||
|
match *level {
|
||||||
|
tracing::Level::ERROR => libc::LOG_ERR,
|
||||||
|
tracing::Level::WARN => libc::LOG_WARNING,
|
||||||
|
tracing::Level::INFO => libc::LOG_INFO,
|
||||||
|
tracing::Level::DEBUG => libc::LOG_DEBUG,
|
||||||
|
tracing::Level::TRACE => libc::LOG_DEBUG,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
self.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 SyslogMakeWriter {
|
||||||
|
type Writer = SyslogWriter;
|
||||||
|
|
||||||
|
fn make_writer(&'a self) -> Self::Writer {
|
||||||
|
SyslogWriter {
|
||||||
|
priority: libc::LOG_INFO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer {
|
||||||
|
SyslogWriter {
|
||||||
|
priority: syslog_priority_for_level(meta.level()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_syslog_priority_for_level_mapping() {
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::ERROR),
|
||||||
|
libc::LOG_ERR
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::WARN),
|
||||||
|
libc::LOG_WARNING
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::INFO),
|
||||||
|
libc::LOG_INFO
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::DEBUG),
|
||||||
|
libc::LOG_DEBUG
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::TRACE),
|
||||||
|
libc::LOG_DEBUG
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-20
@@ -21,10 +21,29 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
if config.general.use_middle_proxy {
|
if config.general.use_middle_proxy {
|
||||||
if let Some(pool) = me_pool.as_ref() {
|
if let Some(pool) = me_pool.as_ref() {
|
||||||
let initial_ready = pool.admission_ready_conditional_cast().await;
|
let initial_ready = pool.admission_ready_conditional_cast().await;
|
||||||
admission_tx.send_replace(initial_ready);
|
let mut fallback_enabled = config.general.me2dc_fallback;
|
||||||
let _ = route_runtime.set_mode(RelayRouteMode::Middle);
|
let mut fast_fallback_enabled = fallback_enabled && config.general.me2dc_fast;
|
||||||
|
let (initial_gate_open, initial_route_mode, initial_fallback_reason) = if initial_ready
|
||||||
|
{
|
||||||
|
(true, RelayRouteMode::Middle, None)
|
||||||
|
} else if fast_fallback_enabled {
|
||||||
|
(
|
||||||
|
true,
|
||||||
|
RelayRouteMode::Direct,
|
||||||
|
Some("fast_not_ready_fallback"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(false, RelayRouteMode::Middle, None)
|
||||||
|
};
|
||||||
|
admission_tx.send_replace(initial_gate_open);
|
||||||
|
let _ = route_runtime.set_mode(initial_route_mode);
|
||||||
if initial_ready {
|
if initial_ready {
|
||||||
info!("Conditional-admission gate: open / ME pool READY");
|
info!("Conditional-admission gate: open / ME pool READY");
|
||||||
|
} else if let Some(reason) = initial_fallback_reason {
|
||||||
|
warn!(
|
||||||
|
fallback_reason = reason,
|
||||||
|
"Conditional-admission gate opened in ME fast fallback mode"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
warn!("Conditional-admission gate: closed / ME pool is NOT ready)");
|
warn!("Conditional-admission gate: closed / ME pool is NOT ready)");
|
||||||
}
|
}
|
||||||
@@ -34,10 +53,9 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
let route_runtime_gate = route_runtime.clone();
|
let route_runtime_gate = route_runtime.clone();
|
||||||
let mut config_rx_gate = config_rx.clone();
|
let mut config_rx_gate = config_rx.clone();
|
||||||
let mut admission_poll_ms = config.general.me_admission_poll_ms.max(1);
|
let mut admission_poll_ms = config.general.me_admission_poll_ms.max(1);
|
||||||
let mut fallback_enabled = config.general.me2dc_fallback;
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut gate_open = initial_ready;
|
let mut gate_open = initial_gate_open;
|
||||||
let mut route_mode = RelayRouteMode::Middle;
|
let mut route_mode = initial_route_mode;
|
||||||
let mut ready_observed = initial_ready;
|
let mut ready_observed = initial_ready;
|
||||||
let mut not_ready_since = if initial_ready {
|
let mut not_ready_since = if initial_ready {
|
||||||
None
|
None
|
||||||
@@ -53,16 +71,23 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
let cfg = config_rx_gate.borrow_and_update().clone();
|
let cfg = config_rx_gate.borrow_and_update().clone();
|
||||||
admission_poll_ms = cfg.general.me_admission_poll_ms.max(1);
|
admission_poll_ms = cfg.general.me_admission_poll_ms.max(1);
|
||||||
fallback_enabled = cfg.general.me2dc_fallback;
|
fallback_enabled = cfg.general.me2dc_fallback;
|
||||||
|
fast_fallback_enabled = cfg.general.me2dc_fallback && cfg.general.me2dc_fast;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_ = tokio::time::sleep(Duration::from_millis(admission_poll_ms)) => {}
|
_ = tokio::time::sleep(Duration::from_millis(admission_poll_ms)) => {}
|
||||||
}
|
}
|
||||||
let ready = pool_for_gate.admission_ready_conditional_cast().await;
|
let ready = pool_for_gate.admission_ready_conditional_cast().await;
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let (next_gate_open, next_route_mode, next_fallback_active) = if ready {
|
let (next_gate_open, next_route_mode, next_fallback_reason) = if ready {
|
||||||
ready_observed = true;
|
ready_observed = true;
|
||||||
not_ready_since = None;
|
not_ready_since = None;
|
||||||
(true, RelayRouteMode::Middle, false)
|
(true, RelayRouteMode::Middle, None)
|
||||||
|
} else if fast_fallback_enabled {
|
||||||
|
(
|
||||||
|
true,
|
||||||
|
RelayRouteMode::Direct,
|
||||||
|
Some("fast_not_ready_fallback"),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
let not_ready_started_at = *not_ready_since.get_or_insert(now);
|
let not_ready_started_at = *not_ready_since.get_or_insert(now);
|
||||||
let not_ready_for = now.saturating_duration_since(not_ready_started_at);
|
let not_ready_for = now.saturating_duration_since(not_ready_started_at);
|
||||||
@@ -72,11 +97,12 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
STARTUP_FALLBACK_AFTER
|
STARTUP_FALLBACK_AFTER
|
||||||
};
|
};
|
||||||
if fallback_enabled && not_ready_for > fallback_after {
|
if fallback_enabled && not_ready_for > fallback_after {
|
||||||
(true, RelayRouteMode::Direct, true)
|
(true, RelayRouteMode::Direct, Some("strict_grace_fallback"))
|
||||||
} else {
|
} else {
|
||||||
(false, RelayRouteMode::Middle, false)
|
(false, RelayRouteMode::Middle, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let next_fallback_active = next_fallback_reason.is_some();
|
||||||
|
|
||||||
if next_route_mode != route_mode {
|
if next_route_mode != route_mode {
|
||||||
route_mode = next_route_mode;
|
route_mode = next_route_mode;
|
||||||
@@ -88,17 +114,28 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
"Middle-End routing restored for new sessions"
|
"Middle-End routing restored for new sessions"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let fallback_after = if ready_observed {
|
let fallback_reason = next_fallback_reason.unwrap_or("unknown");
|
||||||
RUNTIME_FALLBACK_AFTER
|
if fallback_reason == "strict_grace_fallback" {
|
||||||
|
let fallback_after = if ready_observed {
|
||||||
|
RUNTIME_FALLBACK_AFTER
|
||||||
|
} else {
|
||||||
|
STARTUP_FALLBACK_AFTER
|
||||||
|
};
|
||||||
|
warn!(
|
||||||
|
target_mode = route_mode.as_str(),
|
||||||
|
cutover_generation = snapshot.generation,
|
||||||
|
grace_secs = fallback_after.as_secs(),
|
||||||
|
fallback_reason,
|
||||||
|
"ME pool stayed not-ready beyond grace; routing new sessions via Direct-DC"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
STARTUP_FALLBACK_AFTER
|
warn!(
|
||||||
};
|
target_mode = route_mode.as_str(),
|
||||||
warn!(
|
cutover_generation = snapshot.generation,
|
||||||
target_mode = route_mode.as_str(),
|
fallback_reason,
|
||||||
cutover_generation = snapshot.generation,
|
"ME pool not-ready; routing new sessions via Direct-DC (fast mode)"
|
||||||
grace_secs = fallback_after.as_secs(),
|
);
|
||||||
"ME pool stayed not-ready beyond grace; routing new sessions via Direct-DC"
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +145,10 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
admission_tx_gate.send_replace(gate_open);
|
admission_tx_gate.send_replace(gate_open);
|
||||||
if gate_open {
|
if gate_open {
|
||||||
if next_fallback_active {
|
if next_fallback_active {
|
||||||
warn!("Conditional-admission gate opened in ME fallback mode");
|
warn!(
|
||||||
|
fallback_reason = next_fallback_reason.unwrap_or("unknown"),
|
||||||
|
"Conditional-admission gate opened in ME fallback mode"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
info!("Conditional-admission gate opened / ME pool READY");
|
info!("Conditional-admission gate opened / ME pool READY");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -11,10 +13,10 @@ use crate::startup::{
|
|||||||
COMPONENT_DC_CONNECTIVITY_PING, COMPONENT_ME_CONNECTIVITY_PING, COMPONENT_RUNTIME_READY,
|
COMPONENT_DC_CONNECTIVITY_PING, COMPONENT_ME_CONNECTIVITY_PING, COMPONENT_RUNTIME_READY,
|
||||||
StartupTracker,
|
StartupTracker,
|
||||||
};
|
};
|
||||||
|
use crate::transport::UpstreamManager;
|
||||||
use crate::transport::middle_proxy::{
|
use crate::transport::middle_proxy::{
|
||||||
MePingFamily, MePingSample, MePool, format_me_route, format_sample_line, run_me_ping,
|
MePingFamily, MePingSample, MePool, format_me_route, format_sample_line, run_me_ping,
|
||||||
};
|
};
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
|
|
||||||
pub(crate) async fn run_startup_connectivity(
|
pub(crate) async fn run_startup_connectivity(
|
||||||
config: &Arc<ProxyConfig>,
|
config: &Arc<ProxyConfig>,
|
||||||
@@ -47,11 +49,15 @@ pub(crate) async fn run_startup_connectivity(
|
|||||||
|
|
||||||
let v4_ok = me_results.iter().any(|r| {
|
let v4_ok = me_results.iter().any(|r| {
|
||||||
matches!(r.family, MePingFamily::V4)
|
matches!(r.family, MePingFamily::V4)
|
||||||
&& r.samples.iter().any(|s| s.error.is_none() && s.handshake_ms.is_some())
|
&& r.samples
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.error.is_none() && s.handshake_ms.is_some())
|
||||||
});
|
});
|
||||||
let v6_ok = me_results.iter().any(|r| {
|
let v6_ok = me_results.iter().any(|r| {
|
||||||
matches!(r.family, MePingFamily::V6)
|
matches!(r.family, MePingFamily::V6)
|
||||||
&& r.samples.iter().any(|s| s.error.is_none() && s.handshake_ms.is_some())
|
&& r.samples
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.error.is_none() && s.handshake_ms.is_some())
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("================= Telegram ME Connectivity =================");
|
info!("================= Telegram ME Connectivity =================");
|
||||||
@@ -131,8 +137,14 @@ pub(crate) async fn run_startup_connectivity(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
for upstream_result in &ping_results {
|
for upstream_result in &ping_results {
|
||||||
let v6_works = upstream_result.v6_results.iter().any(|r| r.rtt_ms.is_some());
|
let v6_works = upstream_result
|
||||||
let v4_works = upstream_result.v4_results.iter().any(|r| r.rtt_ms.is_some());
|
.v6_results
|
||||||
|
.iter()
|
||||||
|
.any(|r| r.rtt_ms.is_some());
|
||||||
|
let v4_works = upstream_result
|
||||||
|
.v4_results
|
||||||
|
.iter()
|
||||||
|
.any(|r| r.rtt_ms.is_some());
|
||||||
|
|
||||||
if upstream_result.both_available {
|
if upstream_result.both_available {
|
||||||
if prefer_ipv6 {
|
if prefer_ipv6 {
|
||||||
|
|||||||
+210
-45
@@ -1,33 +1,73 @@
|
|||||||
use std::time::Duration;
|
#![allow(clippy::items_after_test_module)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
use tracing::{debug, error, info, warn};
|
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::UpstreamManager;
|
||||||
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_via_upstream, load_proxy_config_cache,
|
||||||
|
save_proxy_config_cache,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn resolve_runtime_config_path(config_path_cli: &str, startup_cwd: &std::path::Path) -> PathBuf {
|
pub(crate) fn resolve_runtime_config_path(
|
||||||
let raw = PathBuf::from(config_path_cli);
|
config_path_cli: &str,
|
||||||
let absolute = if raw.is_absolute() {
|
startup_cwd: &std::path::Path,
|
||||||
raw
|
config_path_explicit: bool,
|
||||||
} else {
|
) -> PathBuf {
|
||||||
startup_cwd.join(raw)
|
if config_path_explicit {
|
||||||
};
|
let raw = PathBuf::from(config_path_cli);
|
||||||
absolute.canonicalize().unwrap_or(absolute)
|
let absolute = if raw.is_absolute() {
|
||||||
|
raw
|
||||||
|
} else {
|
||||||
|
startup_cwd.join(raw)
|
||||||
|
};
|
||||||
|
return absolute.canonicalize().unwrap_or(absolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
let etc_telemt = std::path::Path::new("/etc/telemt");
|
||||||
|
let candidates = [
|
||||||
|
startup_cwd.join("config.toml"),
|
||||||
|
startup_cwd.join("telemt.toml"),
|
||||||
|
etc_telemt.join("telemt.toml"),
|
||||||
|
etc_telemt.join("config.toml"),
|
||||||
|
];
|
||||||
|
for candidate in candidates {
|
||||||
|
if candidate.is_file() {
|
||||||
|
return candidate.canonicalize().unwrap_or(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startup_cwd.join("config.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_cli() -> (String, Option<PathBuf>, bool, Option<String>) {
|
/// Parsed CLI arguments.
|
||||||
|
pub(crate) struct CliArgs {
|
||||||
|
pub config_path: String,
|
||||||
|
pub config_path_explicit: bool,
|
||||||
|
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 config_path_explicit = false;
|
||||||
let mut data_path: Option<PathBuf> = None;
|
let mut data_path: Option<PathBuf> = None;
|
||||||
let mut silent = false;
|
let mut silent = false;
|
||||||
let mut log_level: Option<String> = None;
|
let mut log_level: Option<String> = None;
|
||||||
|
|
||||||
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) {
|
||||||
@@ -50,7 +90,23 @@ pub(crate) fn parse_cli() -> (String, Option<PathBuf>, bool, Option<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
s if s.starts_with("--data-path=") => {
|
s if s.starts_with("--data-path=") => {
|
||||||
data_path = Some(PathBuf::from(s.trim_start_matches("--data-path=").to_string()));
|
data_path = Some(PathBuf::from(
|
||||||
|
s.trim_start_matches("--data-path=").to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--working-dir" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
data_path = Some(PathBuf::from(args[i].clone()));
|
||||||
|
} else {
|
||||||
|
eprintln!("Missing value for --working-dir");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--working-dir=") => {
|
||||||
|
data_path = Some(PathBuf::from(
|
||||||
|
s.trim_start_matches("--working-dir=").to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
"--silent" | "-s" => {
|
"--silent" | "-s" => {
|
||||||
silent = true;
|
silent = true;
|
||||||
@@ -65,36 +121,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('-') => {
|
s if !s.starts_with('-') => {
|
||||||
config_path = s.to_string();
|
if !matches!(s, "run" | "start" | "stop" | "reload" | "status") {
|
||||||
|
config_path = s.to_string();
|
||||||
|
config_path_explicit = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
eprintln!("Unknown option: {}", other);
|
eprintln!("Unknown option: {}", other);
|
||||||
@@ -103,7 +158,75 @@ 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,
|
||||||
|
config_path_explicit,
|
||||||
|
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!(" --working-dir <DIR> Alias for --data-path");
|
||||||
|
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)]
|
||||||
@@ -121,7 +244,7 @@ mod tests {
|
|||||||
let target = startup_cwd.join("config.toml");
|
let target = startup_cwd.join("config.toml");
|
||||||
std::fs::write(&target, " ").unwrap();
|
std::fs::write(&target, " ").unwrap();
|
||||||
|
|
||||||
let resolved = resolve_runtime_config_path("config.toml", &startup_cwd);
|
let resolved = resolve_runtime_config_path("config.toml", &startup_cwd, true);
|
||||||
assert_eq!(resolved, target.canonicalize().unwrap());
|
assert_eq!(resolved, target.canonicalize().unwrap());
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&target);
|
let _ = std::fs::remove_file(&target);
|
||||||
@@ -137,16 +260,55 @@ mod tests {
|
|||||||
let startup_cwd = std::env::temp_dir().join(format!("telemt_cfg_path_missing_{nonce}"));
|
let startup_cwd = std::env::temp_dir().join(format!("telemt_cfg_path_missing_{nonce}"));
|
||||||
std::fs::create_dir_all(&startup_cwd).unwrap();
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
|
||||||
let resolved = resolve_runtime_config_path("missing.toml", &startup_cwd);
|
let resolved = resolve_runtime_config_path("missing.toml", &startup_cwd, true);
|
||||||
assert_eq!(resolved, startup_cwd.join("missing.toml"));
|
assert_eq!(resolved, startup_cwd.join("missing.toml"));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir(&startup_cwd);
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_config_path_uses_startup_candidates_when_not_explicit() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let startup_cwd =
|
||||||
|
std::env::temp_dir().join(format!("telemt_cfg_startup_candidates_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
let telemt = startup_cwd.join("telemt.toml");
|
||||||
|
std::fs::write(&telemt, " ").unwrap();
|
||||||
|
|
||||||
|
let resolved = resolve_runtime_config_path("config.toml", &startup_cwd, false);
|
||||||
|
assert_eq!(resolved, telemt.canonicalize().unwrap());
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&telemt);
|
||||||
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_config_path_defaults_to_startup_config_when_none_found() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let startup_cwd = std::env::temp_dir().join(format!("telemt_cfg_startup_default_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
|
||||||
|
let resolved = resolve_runtime_config_path("config.toml", &startup_cwd, false);
|
||||||
|
assert_eq!(resolved, startup_cwd.join("config.toml"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
|
pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
|
||||||
info!(target: "telemt::links", "--- Proxy Links ({}) ---", host);
|
info!(target: "telemt::links", "--- Proxy Links ({}) ---", host);
|
||||||
for user_name in config.general.links.show.resolve_users(&config.access.users) {
|
for user_name in config
|
||||||
|
.general
|
||||||
|
.links
|
||||||
|
.show
|
||||||
|
.resolve_users(&config.access.users)
|
||||||
|
{
|
||||||
if let Some(secret) = config.access.users.get(user_name) {
|
if let Some(secret) = config.access.users.get(user_name) {
|
||||||
info!(target: "telemt::links", "User: {}", user_name);
|
info!(target: "telemt::links", "User: {}", user_name);
|
||||||
if config.general.modes.classic {
|
if config.general.modes.classic {
|
||||||
@@ -253,6 +415,7 @@ pub(crate) fn format_uptime(total_secs: u64) -> String {
|
|||||||
format!("{} / {} seconds", parts.join(", "), total_secs)
|
format!("{} / {} seconds", parts.join(", "), total_secs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) async fn wait_until_admission_open(admission_rx: &mut watch::Receiver<bool>) -> bool {
|
pub(crate) async fn wait_until_admission_open(admission_rx: &mut watch::Receiver<bool>) -> bool {
|
||||||
loop {
|
loop {
|
||||||
if *admission_rx.borrow() {
|
if *admission_rx.borrow() {
|
||||||
@@ -273,9 +436,10 @@ pub(crate) async fn load_startup_proxy_config_snapshot(
|
|||||||
cache_path: Option<&str>,
|
cache_path: Option<&str>,
|
||||||
me2dc_fallback: bool,
|
me2dc_fallback: bool,
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
|
upstream: Option<std::sync::Arc<UpstreamManager>>,
|
||||||
) -> Option<ProxyConfigData> {
|
) -> Option<ProxyConfigData> {
|
||||||
loop {
|
loop {
|
||||||
match fetch_proxy_config_with_raw(url).await {
|
match fetch_proxy_config_with_raw_via_upstream(url, upstream.clone()).await {
|
||||||
Ok((cfg, raw)) => {
|
Ok((cfg, raw)) => {
|
||||||
if !cfg.map.is_empty() {
|
if !cfg.map.is_empty() {
|
||||||
if let Some(path) = cache_path
|
if let Some(path) = cache_path
|
||||||
@@ -286,7 +450,10 @@ pub(crate) async fn load_startup_proxy_config_snapshot(
|
|||||||
return Some(cfg);
|
return Some(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
warn!(snapshot = label, url, "Startup proxy-config is empty; trying disk cache");
|
warn!(
|
||||||
|
snapshot = label,
|
||||||
|
url, "Startup proxy-config is empty; trying disk cache"
|
||||||
|
);
|
||||||
if let Some(path) = cache_path {
|
if let Some(path) = cache_path {
|
||||||
match load_proxy_config_cache(path).await {
|
match load_proxy_config_cache(path).await {
|
||||||
Ok(cached) if !cached.map.is_empty() => {
|
Ok(cached) if !cached.map.is_empty() => {
|
||||||
@@ -301,8 +468,7 @@ pub(crate) async fn load_startup_proxy_config_snapshot(
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
warn!(
|
warn!(
|
||||||
snapshot = label,
|
snapshot = label,
|
||||||
path,
|
path, "Startup proxy-config cache is empty; ignoring cache file"
|
||||||
"Startup proxy-config cache is empty; ignoring cache file"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(cache_err) => {
|
Err(cache_err) => {
|
||||||
@@ -346,8 +512,7 @@ pub(crate) async fn load_startup_proxy_config_snapshot(
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
warn!(
|
warn!(
|
||||||
snapshot = label,
|
snapshot = label,
|
||||||
path,
|
path, "Startup proxy-config cache is empty; ignoring cache file"
|
||||||
"Startup proxy-config cache is empty; ignoring cache file"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(cache_err) => {
|
Err(cache_err) => {
|
||||||
|
|||||||
+119
-38
@@ -12,19 +12,18 @@ use tracing::{debug, error, info, warn};
|
|||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::proxy::route_mode::{ROUTE_SWITCH_ERROR_MSG, RouteRuntimeController};
|
|
||||||
use crate::proxy::ClientHandler;
|
use crate::proxy::ClientHandler;
|
||||||
|
use crate::proxy::route_mode::{ROUTE_SWITCH_ERROR_MSG, RouteRuntimeController};
|
||||||
|
use crate::proxy::shared_state::ProxySharedState;
|
||||||
use crate::startup::{COMPONENT_LISTENERS_BIND, StartupTracker};
|
use crate::startup::{COMPONENT_LISTENERS_BIND, StartupTracker};
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::stats::{ReplayChecker, Stats};
|
use crate::stats::{ReplayChecker, Stats};
|
||||||
use crate::stream::BufferPool;
|
use crate::stream::BufferPool;
|
||||||
use crate::tls_front::TlsFrontCache;
|
use crate::tls_front::TlsFrontCache;
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
use crate::transport::{
|
use crate::transport::{ListenOptions, UpstreamManager, create_listener, find_listener_processes};
|
||||||
ListenOptions, UpstreamManager, create_listener, find_listener_processes,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::helpers::{is_expected_handshake_eof, print_proxy_links, wait_until_admission_open};
|
use super::helpers::{is_expected_handshake_eof, print_proxy_links};
|
||||||
|
|
||||||
pub(crate) struct BoundListeners {
|
pub(crate) struct BoundListeners {
|
||||||
pub(crate) listeners: Vec<(TcpListener, bool)>,
|
pub(crate) listeners: Vec<(TcpListener, bool)>,
|
||||||
@@ -51,6 +50,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
beobachten: Arc<BeobachtenStore>,
|
beobachten: Arc<BeobachtenStore>,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
max_connections: Arc<Semaphore>,
|
max_connections: Arc<Semaphore>,
|
||||||
) -> Result<BoundListeners, Box<dyn Error>> {
|
) -> Result<BoundListeners, Box<dyn Error>> {
|
||||||
startup_tracker
|
startup_tracker
|
||||||
@@ -74,6 +74,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
let options = ListenOptions {
|
let options = ListenOptions {
|
||||||
reuse_port: listener_conf.reuse_allow,
|
reuse_port: listener_conf.reuse_allow,
|
||||||
ipv6_only: listener_conf.ip.is_ipv6(),
|
ipv6_only: listener_conf.ip.is_ipv6(),
|
||||||
|
backlog: config.server.listen_backlog,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,8 +82,9 @@ pub(crate) async fn bind_listeners(
|
|||||||
Ok(socket) => {
|
Ok(socket) => {
|
||||||
let listener = TcpListener::from_std(socket.into())?;
|
let listener = TcpListener::from_std(socket.into())?;
|
||||||
info!("Listening on {}", addr);
|
info!("Listening on {}", addr);
|
||||||
let listener_proxy_protocol =
|
let listener_proxy_protocol = listener_conf
|
||||||
listener_conf.proxy_protocol.unwrap_or(config.server.proxy_protocol);
|
.proxy_protocol
|
||||||
|
.unwrap_or(config.server.proxy_protocol);
|
||||||
|
|
||||||
let public_host = if let Some(ref announce) = listener_conf.announce {
|
let public_host = if let Some(ref announce) = listener_conf.announce {
|
||||||
announce.clone()
|
announce.clone()
|
||||||
@@ -100,8 +102,14 @@ pub(crate) async fn bind_listeners(
|
|||||||
listener_conf.ip.to_string()
|
listener_conf.ip.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
if config.general.links.public_host.is_none() && !config.general.links.show.is_empty() {
|
if config.general.links.public_host.is_none()
|
||||||
let link_port = config.general.links.public_port.unwrap_or(config.server.port);
|
&& !config.general.links.show.is_empty()
|
||||||
|
{
|
||||||
|
let link_port = config
|
||||||
|
.general
|
||||||
|
.links
|
||||||
|
.public_port
|
||||||
|
.unwrap_or(config.server.port);
|
||||||
print_proxy_links(&public_host, link_port, config);
|
print_proxy_links(&public_host, link_port, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,12 +153,14 @@ pub(crate) async fn bind_listeners(
|
|||||||
let (host, port) = if let Some(ref h) = config.general.links.public_host {
|
let (host, port) = if let Some(ref h) = config.general.links.public_host {
|
||||||
(
|
(
|
||||||
h.clone(),
|
h.clone(),
|
||||||
config.general.links.public_port.unwrap_or(config.server.port),
|
config
|
||||||
|
.general
|
||||||
|
.links
|
||||||
|
.public_port
|
||||||
|
.unwrap_or(config.server.port),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let ip = detected_ip_v4
|
let ip = detected_ip_v4.or(detected_ip_v6).map(|ip| ip.to_string());
|
||||||
.or(detected_ip_v6)
|
|
||||||
.map(|ip| ip.to_string());
|
|
||||||
if ip.is_none() {
|
if ip.is_none() {
|
||||||
warn!(
|
warn!(
|
||||||
"show_link is configured but public IP could not be detected. Set public_host in config."
|
"show_link is configured but public IP could not be detected. Set public_host in config."
|
||||||
@@ -158,7 +168,11 @@ pub(crate) async fn bind_listeners(
|
|||||||
}
|
}
|
||||||
(
|
(
|
||||||
ip.unwrap_or_else(|| "UNKNOWN".to_string()),
|
ip.unwrap_or_else(|| "UNKNOWN".to_string()),
|
||||||
config.general.links.public_port.unwrap_or(config.server.port),
|
config
|
||||||
|
.general
|
||||||
|
.links
|
||||||
|
.public_port
|
||||||
|
.unwrap_or(config.server.port),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -178,13 +192,19 @@ pub(crate) async fn bind_listeners(
|
|||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
let perms = std::fs::Permissions::from_mode(mode);
|
let perms = std::fs::Permissions::from_mode(mode);
|
||||||
if let Err(e) = std::fs::set_permissions(unix_path, perms) {
|
if let Err(e) = std::fs::set_permissions(unix_path, perms) {
|
||||||
error!("Failed to set unix socket permissions to {}: {}", perm_str, e);
|
error!(
|
||||||
|
"Failed to set unix socket permissions to {}: {}",
|
||||||
|
perm_str, e
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
info!("Listening on unix:{} (mode {})", unix_path, perm_str);
|
info!("Listening on unix:{} (mode {})", unix_path, perm_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Invalid listen_unix_sock_perm '{}': {}. Ignoring.", perm_str, e);
|
warn!(
|
||||||
|
"Invalid listen_unix_sock_perm '{}': {}. Ignoring.",
|
||||||
|
perm_str, e
|
||||||
|
);
|
||||||
info!("Listening on unix:{}", unix_path);
|
info!("Listening on unix:{}", unix_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +215,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
has_unix_listener = true;
|
has_unix_listener = true;
|
||||||
|
|
||||||
let mut config_rx_unix: watch::Receiver<Arc<ProxyConfig>> = config_rx.clone();
|
let mut config_rx_unix: watch::Receiver<Arc<ProxyConfig>> = config_rx.clone();
|
||||||
let mut admission_rx_unix = admission_rx.clone();
|
let admission_rx_unix = admission_rx.clone();
|
||||||
let stats = stats.clone();
|
let stats = stats.clone();
|
||||||
let upstream_manager = upstream_manager.clone();
|
let upstream_manager = upstream_manager.clone();
|
||||||
let replay_checker = replay_checker.clone();
|
let replay_checker = replay_checker.clone();
|
||||||
@@ -206,23 +226,50 @@ pub(crate) async fn bind_listeners(
|
|||||||
let tls_cache = tls_cache.clone();
|
let tls_cache = tls_cache.clone();
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
let beobachten = beobachten.clone();
|
let beobachten = beobachten.clone();
|
||||||
|
let shared = shared.clone();
|
||||||
let max_connections_unix = max_connections.clone();
|
let max_connections_unix = max_connections.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let unix_conn_counter = Arc::new(std::sync::atomic::AtomicU64::new(1));
|
let unix_conn_counter = Arc::new(std::sync::atomic::AtomicU64::new(1));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if !wait_until_admission_open(&mut admission_rx_unix).await {
|
|
||||||
warn!("Conditional-admission gate channel closed for unix listener");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match unix_listener.accept().await {
|
match unix_listener.accept().await {
|
||||||
Ok((stream, _)) => {
|
Ok((stream, _)) => {
|
||||||
let permit = match max_connections_unix.clone().acquire_owned().await {
|
if !*admission_rx_unix.borrow() {
|
||||||
Ok(permit) => permit,
|
drop(stream);
|
||||||
Err(_) => {
|
continue;
|
||||||
error!("Connection limiter is closed");
|
}
|
||||||
break;
|
let accept_permit_timeout_ms =
|
||||||
|
config_rx_unix.borrow().server.accept_permit_timeout_ms;
|
||||||
|
let permit = if accept_permit_timeout_ms == 0 {
|
||||||
|
match max_connections_unix.clone().acquire_owned().await {
|
||||||
|
Ok(permit) => permit,
|
||||||
|
Err(_) => {
|
||||||
|
error!("Connection limiter is closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match tokio::time::timeout(
|
||||||
|
Duration::from_millis(accept_permit_timeout_ms),
|
||||||
|
max_connections_unix.clone().acquire_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(permit)) => permit,
|
||||||
|
Ok(Err(_)) => {
|
||||||
|
error!("Connection limiter is closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
stats.increment_accept_permit_timeout_total();
|
||||||
|
debug!(
|
||||||
|
timeout_ms = accept_permit_timeout_ms,
|
||||||
|
"Dropping accepted unix connection: permit wait timeout"
|
||||||
|
);
|
||||||
|
drop(stream);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let conn_id =
|
let conn_id =
|
||||||
@@ -241,11 +288,12 @@ pub(crate) async fn bind_listeners(
|
|||||||
let tls_cache = tls_cache.clone();
|
let tls_cache = tls_cache.clone();
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
let beobachten = beobachten.clone();
|
let beobachten = beobachten.clone();
|
||||||
|
let shared = shared.clone();
|
||||||
let proxy_protocol_enabled = config.server.proxy_protocol;
|
let proxy_protocol_enabled = config.server.proxy_protocol;
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _permit = permit;
|
let _permit = permit;
|
||||||
if let Err(e) = crate::proxy::client::handle_client_stream(
|
if let Err(e) = crate::proxy::client::handle_client_stream_with_shared(
|
||||||
stream,
|
stream,
|
||||||
fake_peer,
|
fake_peer,
|
||||||
config,
|
config,
|
||||||
@@ -259,6 +307,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
tls_cache,
|
tls_cache,
|
||||||
ip_tracker,
|
ip_tracker,
|
||||||
beobachten,
|
beobachten,
|
||||||
|
shared,
|
||||||
proxy_protocol_enabled,
|
proxy_protocol_enabled,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -308,11 +357,12 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
tls_cache: Option<Arc<TlsFrontCache>>,
|
tls_cache: Option<Arc<TlsFrontCache>>,
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
beobachten: Arc<BeobachtenStore>,
|
beobachten: Arc<BeobachtenStore>,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
max_connections: Arc<Semaphore>,
|
max_connections: Arc<Semaphore>,
|
||||||
) {
|
) {
|
||||||
for (listener, listener_proxy_protocol) in listeners {
|
for (listener, listener_proxy_protocol) in listeners {
|
||||||
let mut config_rx: watch::Receiver<Arc<ProxyConfig>> = config_rx.clone();
|
let mut config_rx: watch::Receiver<Arc<ProxyConfig>> = config_rx.clone();
|
||||||
let mut admission_rx_tcp = admission_rx.clone();
|
let admission_rx_tcp = admission_rx.clone();
|
||||||
let stats = stats.clone();
|
let stats = stats.clone();
|
||||||
let upstream_manager = upstream_manager.clone();
|
let upstream_manager = upstream_manager.clone();
|
||||||
let replay_checker = replay_checker.clone();
|
let replay_checker = replay_checker.clone();
|
||||||
@@ -323,21 +373,50 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
let tls_cache = tls_cache.clone();
|
let tls_cache = tls_cache.clone();
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
let beobachten = beobachten.clone();
|
let beobachten = beobachten.clone();
|
||||||
|
let shared = shared.clone();
|
||||||
let max_connections_tcp = max_connections.clone();
|
let max_connections_tcp = max_connections.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
if !wait_until_admission_open(&mut admission_rx_tcp).await {
|
|
||||||
warn!("Conditional-admission gate channel closed for tcp listener");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
Ok((stream, peer_addr)) => {
|
Ok((stream, peer_addr)) => {
|
||||||
let permit = match max_connections_tcp.clone().acquire_owned().await {
|
if !*admission_rx_tcp.borrow() {
|
||||||
Ok(permit) => permit,
|
debug!(peer = %peer_addr, "Admission gate closed, dropping connection");
|
||||||
Err(_) => {
|
drop(stream);
|
||||||
error!("Connection limiter is closed");
|
continue;
|
||||||
break;
|
}
|
||||||
|
let accept_permit_timeout_ms =
|
||||||
|
config_rx.borrow().server.accept_permit_timeout_ms;
|
||||||
|
let permit = if accept_permit_timeout_ms == 0 {
|
||||||
|
match max_connections_tcp.clone().acquire_owned().await {
|
||||||
|
Ok(permit) => permit,
|
||||||
|
Err(_) => {
|
||||||
|
error!("Connection limiter is closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match tokio::time::timeout(
|
||||||
|
Duration::from_millis(accept_permit_timeout_ms),
|
||||||
|
max_connections_tcp.clone().acquire_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(permit)) => permit,
|
||||||
|
Ok(Err(_)) => {
|
||||||
|
error!("Connection limiter is closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
stats.increment_accept_permit_timeout_total();
|
||||||
|
debug!(
|
||||||
|
peer = %peer_addr,
|
||||||
|
timeout_ms = accept_permit_timeout_ms,
|
||||||
|
"Dropping accepted connection: permit wait timeout"
|
||||||
|
);
|
||||||
|
drop(stream);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config = config_rx.borrow_and_update().clone();
|
let config = config_rx.borrow_and_update().clone();
|
||||||
@@ -351,13 +430,14 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
let tls_cache = tls_cache.clone();
|
let tls_cache = tls_cache.clone();
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
let beobachten = beobachten.clone();
|
let beobachten = beobachten.clone();
|
||||||
|
let shared = shared.clone();
|
||||||
let proxy_protocol_enabled = listener_proxy_protocol;
|
let proxy_protocol_enabled = listener_proxy_protocol;
|
||||||
let real_peer_report = Arc::new(std::sync::Mutex::new(None));
|
let real_peer_report = Arc::new(std::sync::Mutex::new(None));
|
||||||
let real_peer_report_for_handler = real_peer_report.clone();
|
let real_peer_report_for_handler = real_peer_report.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _permit = permit;
|
let _permit = permit;
|
||||||
if let Err(e) = ClientHandler::new(
|
if let Err(e) = ClientHandler::new_with_shared(
|
||||||
stream,
|
stream,
|
||||||
peer_addr,
|
peer_addr,
|
||||||
config,
|
config,
|
||||||
@@ -371,6 +451,7 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
tls_cache,
|
tls_cache,
|
||||||
ip_tracker,
|
ip_tracker,
|
||||||
beobachten,
|
beobachten,
|
||||||
|
shared,
|
||||||
proxy_protocol_enabled,
|
proxy_protocol_enabled,
|
||||||
real_peer_report_for_handler,
|
real_peer_report_for_handler,
|
||||||
)
|
)
|
||||||
|
|||||||
+150
-20
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -12,8 +14,8 @@ use crate::startup::{
|
|||||||
COMPONENT_ME_PROXY_CONFIG_V6, COMPONENT_ME_SECRET_FETCH, StartupMeStatus, StartupTracker,
|
COMPONENT_ME_PROXY_CONFIG_V6, COMPONENT_ME_SECRET_FETCH, StartupMeStatus, StartupTracker,
|
||||||
};
|
};
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::transport::middle_proxy::MePool;
|
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
use crate::transport::middle_proxy::MePool;
|
||||||
|
|
||||||
use super::helpers::load_startup_proxy_config_snapshot;
|
use super::helpers::load_startup_proxy_config_snapshot;
|
||||||
|
|
||||||
@@ -61,9 +63,10 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
let proxy_secret_path = config.general.proxy_secret_path.as_deref();
|
let proxy_secret_path = config.general.proxy_secret_path.as_deref();
|
||||||
let pool_size = config.general.middle_proxy_pool_size.max(1);
|
let pool_size = config.general.middle_proxy_pool_size.max(1);
|
||||||
let proxy_secret = loop {
|
let proxy_secret = loop {
|
||||||
match crate::transport::middle_proxy::fetch_proxy_secret(
|
match crate::transport::middle_proxy::fetch_proxy_secret_with_upstream(
|
||||||
proxy_secret_path,
|
proxy_secret_path,
|
||||||
config.general.proxy_secret_len_max,
|
config.general.proxy_secret_len_max,
|
||||||
|
Some(upstream_manager.clone()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -127,6 +130,7 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
config.general.proxy_config_v4_cache_path.as_deref(),
|
config.general.proxy_config_v4_cache_path.as_deref(),
|
||||||
me2dc_fallback,
|
me2dc_fallback,
|
||||||
"getProxyConfig",
|
"getProxyConfig",
|
||||||
|
Some(upstream_manager.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if cfg_v4.is_some() {
|
if cfg_v4.is_some() {
|
||||||
@@ -158,6 +162,7 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
config.general.proxy_config_v6_cache_path.as_deref(),
|
config.general.proxy_config_v6_cache_path.as_deref(),
|
||||||
me2dc_fallback,
|
me2dc_fallback,
|
||||||
"getProxyConfigV6",
|
"getProxyConfigV6",
|
||||||
|
Some(upstream_manager.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if cfg_v6.is_some() {
|
if cfg_v6.is_some() {
|
||||||
@@ -229,15 +234,25 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
config.general.me_adaptive_floor_recover_grace_secs,
|
config.general.me_adaptive_floor_recover_grace_secs,
|
||||||
config.general.me_adaptive_floor_writers_per_core_total,
|
config.general.me_adaptive_floor_writers_per_core_total,
|
||||||
config.general.me_adaptive_floor_cpu_cores_override,
|
config.general.me_adaptive_floor_cpu_cores_override,
|
||||||
config.general.me_adaptive_floor_max_extra_writers_single_per_core,
|
config
|
||||||
config.general.me_adaptive_floor_max_extra_writers_multi_per_core,
|
.general
|
||||||
|
.me_adaptive_floor_max_extra_writers_single_per_core,
|
||||||
|
config
|
||||||
|
.general
|
||||||
|
.me_adaptive_floor_max_extra_writers_multi_per_core,
|
||||||
config.general.me_adaptive_floor_max_active_writers_per_core,
|
config.general.me_adaptive_floor_max_active_writers_per_core,
|
||||||
config.general.me_adaptive_floor_max_warm_writers_per_core,
|
config.general.me_adaptive_floor_max_warm_writers_per_core,
|
||||||
config.general.me_adaptive_floor_max_active_writers_global,
|
config.general.me_adaptive_floor_max_active_writers_global,
|
||||||
config.general.me_adaptive_floor_max_warm_writers_global,
|
config.general.me_adaptive_floor_max_warm_writers_global,
|
||||||
config.general.hardswap,
|
config.general.hardswap,
|
||||||
config.general.me_pool_drain_ttl_secs,
|
config.general.me_pool_drain_ttl_secs,
|
||||||
|
config.general.me_instadrain,
|
||||||
config.general.me_pool_drain_threshold,
|
config.general.me_pool_drain_threshold,
|
||||||
|
config.general.me_pool_drain_soft_evict_enabled,
|
||||||
|
config.general.me_pool_drain_soft_evict_grace_secs,
|
||||||
|
config.general.me_pool_drain_soft_evict_per_writer,
|
||||||
|
config.general.me_pool_drain_soft_evict_budget_per_core,
|
||||||
|
config.general.me_pool_drain_soft_evict_cooldown_ms,
|
||||||
config.general.effective_me_pool_force_close_secs(),
|
config.general.effective_me_pool_force_close_secs(),
|
||||||
config.general.me_pool_min_fresh_ratio,
|
config.general.me_pool_min_fresh_ratio,
|
||||||
config.general.me_hardswap_warmup_delay_min_ms,
|
config.general.me_hardswap_warmup_delay_min_ms,
|
||||||
@@ -262,6 +277,8 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
config.general.me_warn_rate_limit_ms,
|
config.general.me_warn_rate_limit_ms,
|
||||||
config.general.me_route_no_writer_mode,
|
config.general.me_route_no_writer_mode,
|
||||||
config.general.me_route_no_writer_wait_ms,
|
config.general.me_route_no_writer_wait_ms,
|
||||||
|
config.general.me_route_hybrid_max_wait_ms,
|
||||||
|
config.general.me_route_blocking_send_timeout_ms,
|
||||||
config.general.me_route_inline_recovery_attempts,
|
config.general.me_route_inline_recovery_attempts,
|
||||||
config.general.me_route_inline_recovery_wait_ms,
|
config.general.me_route_inline_recovery_wait_ms,
|
||||||
);
|
);
|
||||||
@@ -324,18 +341,76 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
"Middle-End pool initialized successfully"
|
"Middle-End pool initialized successfully"
|
||||||
);
|
);
|
||||||
|
|
||||||
let pool_health = pool_bg.clone();
|
// ── Supervised background tasks ──────────────────
|
||||||
let rng_health = rng_bg.clone();
|
// Each task runs inside a nested tokio::spawn so
|
||||||
let min_conns = pool_size;
|
// that a panic is caught via JoinHandle and the
|
||||||
tokio::spawn(async move {
|
// outer loop restarts the task automatically.
|
||||||
crate::transport::middle_proxy::me_health_monitor(
|
let pool_health = pool_bg.clone();
|
||||||
pool_health,
|
let rng_health = rng_bg.clone();
|
||||||
rng_health,
|
let min_conns = pool_size;
|
||||||
min_conns,
|
tokio::spawn(async move {
|
||||||
)
|
loop {
|
||||||
.await;
|
let p = pool_health.clone();
|
||||||
});
|
let r = rng_health.clone();
|
||||||
break;
|
let res = tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_health_monitor(
|
||||||
|
p, r, min_conns,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(()) => warn!("me_health_monitor exited unexpectedly, restarting"),
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "me_health_monitor panicked, restarting in 1s");
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let pool_drain_enforcer = pool_bg.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let p = pool_drain_enforcer.clone();
|
||||||
|
let res = tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_drain_timeout_enforcer(p).await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(()) => warn!("me_drain_timeout_enforcer exited unexpectedly, restarting"),
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "me_drain_timeout_enforcer panicked, restarting in 1s");
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let pool_watchdog = pool_bg.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let p = pool_watchdog.clone();
|
||||||
|
let res = tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_zombie_writer_watchdog(p).await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(()) => warn!("me_zombie_writer_watchdog exited unexpectedly, restarting"),
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "me_zombie_writer_watchdog panicked, restarting in 1s");
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// CRITICAL: keep the current-thread runtime
|
||||||
|
// alive. Without this, block_on() returns,
|
||||||
|
// the Runtime is dropped, and ALL spawned
|
||||||
|
// background tasks (health monitor, drain
|
||||||
|
// enforcer, zombie watchdog) are silently
|
||||||
|
// cancelled — causing the draining-writer
|
||||||
|
// leak that brought us here.
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
unreachable!();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
startup_tracker_bg.set_me_last_error(Some(e.to_string())).await;
|
startup_tracker_bg.set_me_last_error(Some(e.to_string())).await;
|
||||||
@@ -393,14 +468,69 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
"Middle-End pool initialized successfully"
|
"Middle-End pool initialized successfully"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── Supervised background tasks ──────────────────
|
||||||
let pool_clone = pool.clone();
|
let pool_clone = pool.clone();
|
||||||
let rng_clone = rng.clone();
|
let rng_clone = rng.clone();
|
||||||
let min_conns = pool_size;
|
let min_conns = pool_size;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
crate::transport::middle_proxy::me_health_monitor(
|
loop {
|
||||||
pool_clone, rng_clone, min_conns,
|
let p = pool_clone.clone();
|
||||||
)
|
let r = rng_clone.clone();
|
||||||
.await;
|
let res = tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_health_monitor(
|
||||||
|
p, r, min_conns,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(()) => warn!(
|
||||||
|
"me_health_monitor exited unexpectedly, restarting"
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "me_health_monitor panicked, restarting in 1s");
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let pool_drain_enforcer = pool.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let p = pool_drain_enforcer.clone();
|
||||||
|
let res = tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_drain_timeout_enforcer(p).await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(()) => warn!(
|
||||||
|
"me_drain_timeout_enforcer exited unexpectedly, restarting"
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "me_drain_timeout_enforcer panicked, restarting in 1s");
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let pool_watchdog = pool.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let p = pool_watchdog.clone();
|
||||||
|
let res = tokio::spawn(async move {
|
||||||
|
crate::transport::middle_proxy::me_zombie_writer_watchdog(p).await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(()) => warn!(
|
||||||
|
"me_zombie_writer_watchdog exited unexpectedly, restarting"
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "me_zombie_writer_watchdog panicked, restarting in 1s");
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
break Some(pool);
|
break Some(pool);
|
||||||
|
|||||||
+265
-55
@@ -11,9 +11,9 @@
|
|||||||
// - admission: conditional-cast gate and route mode switching.
|
// - admission: conditional-cast gate and route mode switching.
|
||||||
// - listeners: TCP/Unix listener bind and accept-loop orchestration.
|
// - listeners: TCP/Unix listener bind and accept-loop orchestration.
|
||||||
// - shutdown: graceful shutdown sequence and uptime logging.
|
// - shutdown: graceful shutdown sequence and uptime logging.
|
||||||
mod helpers;
|
|
||||||
mod admission;
|
mod admission;
|
||||||
mod connectivity;
|
mod connectivity;
|
||||||
|
mod helpers;
|
||||||
mod listeners;
|
mod listeners;
|
||||||
mod me_startup;
|
mod me_startup;
|
||||||
mod runtime_tasks;
|
mod runtime_tasks;
|
||||||
@@ -29,26 +29,75 @@ use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload};
|
|||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::config::{LogLevel, ProxyConfig};
|
use crate::config::{LogLevel, ProxyConfig};
|
||||||
|
use crate::conntrack_control;
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::network::probe::{decide_network_capabilities, log_probe_result, run_probe};
|
use crate::network::probe::{decide_network_capabilities, log_probe_result, run_probe};
|
||||||
use crate::proxy::route_mode::{RelayRouteMode, RouteRuntimeController};
|
use crate::proxy::route_mode::{RelayRouteMode, RouteRuntimeController};
|
||||||
|
use crate::proxy::shared_state::ProxySharedState;
|
||||||
|
use crate::startup::{
|
||||||
|
COMPONENT_API_BOOTSTRAP, COMPONENT_CONFIG_LOAD, COMPONENT_ME_POOL_CONSTRUCT,
|
||||||
|
COMPONENT_ME_POOL_INIT_STAGE1, COMPONENT_ME_PROXY_CONFIG_V4, COMPONENT_ME_PROXY_CONFIG_V6,
|
||||||
|
COMPONENT_ME_SECRET_FETCH, COMPONENT_NETWORK_PROBE, COMPONENT_TRACING_INIT, StartupMeStatus,
|
||||||
|
StartupTracker,
|
||||||
|
};
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::stats::telemetry::TelemetryPolicy;
|
use crate::stats::telemetry::TelemetryPolicy;
|
||||||
use crate::stats::{ReplayChecker, Stats};
|
use crate::stats::{ReplayChecker, Stats};
|
||||||
use crate::startup::{
|
|
||||||
COMPONENT_API_BOOTSTRAP, COMPONENT_CONFIG_LOAD,
|
|
||||||
COMPONENT_ME_POOL_CONSTRUCT, COMPONENT_ME_POOL_INIT_STAGE1,
|
|
||||||
COMPONENT_ME_PROXY_CONFIG_V4, COMPONENT_ME_PROXY_CONFIG_V6, COMPONENT_ME_SECRET_FETCH,
|
|
||||||
COMPONENT_NETWORK_PROBE, COMPONENT_TRACING_INIT, StartupMeStatus, StartupTracker,
|
|
||||||
};
|
|
||||||
use crate::stream::BufferPool;
|
use crate::stream::BufferPool;
|
||||||
use crate::transport::middle_proxy::MePool;
|
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
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)
|
||||||
@@ -56,9 +105,18 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
.as_secs();
|
.as_secs();
|
||||||
let startup_tracker = Arc::new(StartupTracker::new(process_started_at_epoch_secs));
|
let startup_tracker = Arc::new(StartupTracker::new(process_started_at_epoch_secs));
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(COMPONENT_CONFIG_LOAD, Some("load and validate config".to_string()))
|
.start_component(
|
||||||
|
COMPONENT_CONFIG_LOAD,
|
||||||
|
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 config_path_explicit = cli_args.config_path_explicit;
|
||||||
|
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) => {
|
||||||
@@ -66,7 +124,8 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config_path = resolve_runtime_config_path(&config_path_cli, &startup_cwd);
|
let mut config_path =
|
||||||
|
resolve_runtime_config_path(&config_path_cli, &startup_cwd, config_path_explicit);
|
||||||
|
|
||||||
let mut config = match ProxyConfig::load(&config_path) {
|
let mut config = match ProxyConfig::load(&config_path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -76,8 +135,99 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
} else {
|
} else {
|
||||||
let default = ProxyConfig::default();
|
let default = ProxyConfig::default();
|
||||||
std::fs::write(&config_path, toml::to_string_pretty(&default).unwrap()).unwrap();
|
|
||||||
eprintln!("[telemt] Created default config at {}", config_path.display());
|
let serialized =
|
||||||
|
match toml::to_string_pretty(&default).or_else(|_| toml::to_string(&default)) {
|
||||||
|
Ok(value) => Some(value),
|
||||||
|
Err(serialize_error) => {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: failed to serialize default config: {}",
|
||||||
|
serialize_error
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if config_path_explicit {
|
||||||
|
if let Some(serialized) = serialized.as_ref() {
|
||||||
|
if let Err(write_error) = std::fs::write(&config_path, serialized) {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Error: failed to create explicit config at {}: {}",
|
||||||
|
config_path.display(),
|
||||||
|
write_error
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Created default config at {}",
|
||||||
|
config_path.display()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: running with in-memory default config without writing to disk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let system_dir = std::path::Path::new("/etc/telemt");
|
||||||
|
let system_config_path = system_dir.join("telemt.toml");
|
||||||
|
let startup_config_path = startup_cwd.join("config.toml");
|
||||||
|
let mut persisted = false;
|
||||||
|
|
||||||
|
if let Some(serialized) = serialized.as_ref() {
|
||||||
|
match std::fs::create_dir_all(system_dir) {
|
||||||
|
Ok(()) => match std::fs::write(&system_config_path, serialized) {
|
||||||
|
Ok(()) => {
|
||||||
|
config_path = system_config_path;
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Created default config at {}",
|
||||||
|
config_path.display()
|
||||||
|
);
|
||||||
|
persisted = true;
|
||||||
|
}
|
||||||
|
Err(write_error) => {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: failed to write default config at {}: {}",
|
||||||
|
system_config_path.display(),
|
||||||
|
write_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(create_error) => {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: failed to create {}: {}",
|
||||||
|
system_dir.display(),
|
||||||
|
create_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !persisted {
|
||||||
|
match std::fs::write(&startup_config_path, serialized) {
|
||||||
|
Ok(()) => {
|
||||||
|
config_path = startup_config_path;
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Created default config at {}",
|
||||||
|
config_path.display()
|
||||||
|
);
|
||||||
|
persisted = true;
|
||||||
|
}
|
||||||
|
Err(write_error) => {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: failed to write default config at {}: {}",
|
||||||
|
startup_config_path.display(),
|
||||||
|
write_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !persisted {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: running with in-memory default config without writing to disk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
default
|
default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,24 +244,36 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
if let Some(ref data_path) = config.general.data_path {
|
if let Some(ref data_path) = config.general.data_path {
|
||||||
if !data_path.is_absolute() {
|
if !data_path.is_absolute() {
|
||||||
eprintln!("[telemt] data_path must be absolute: {}", data_path.display());
|
eprintln!(
|
||||||
|
"[telemt] data_path must be absolute: {}",
|
||||||
|
data_path.display()
|
||||||
|
);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if data_path.exists() {
|
if data_path.exists() {
|
||||||
if !data_path.is_dir() {
|
if !data_path.is_dir() {
|
||||||
eprintln!("[telemt] data_path exists but is not a directory: {}", data_path.display());
|
eprintln!(
|
||||||
std::process::exit(1);
|
"[telemt] data_path exists but is not a directory: {}",
|
||||||
}
|
data_path.display()
|
||||||
} else {
|
);
|
||||||
if let Err(e) = std::fs::create_dir_all(data_path) {
|
|
||||||
eprintln!("[telemt] Can't create data_path {}: {}", data_path.display(), e);
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
} else if let Err(e) = std::fs::create_dir_all(data_path) {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Can't create data_path {}: {}",
|
||||||
|
data_path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = std::env::set_current_dir(data_path) {
|
if let Err(e) = std::env::set_current_dir(data_path) {
|
||||||
eprintln!("[telemt] Can't use data_path {}: {}", data_path.display(), e);
|
eprintln!(
|
||||||
|
"[telemt] Can't use data_path {}: {}",
|
||||||
|
data_path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,22 +297,54 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new("info"));
|
let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new("info"));
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(COMPONENT_TRACING_INIT, Some("initialize tracing subscriber".to_string()))
|
.start_component(
|
||||||
|
COMPONENT_TRACING_INIT,
|
||||||
|
Some("initialize tracing subscriber".to_string()),
|
||||||
|
)
|
||||||
.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(COMPONENT_TRACING_INIT, Some("tracing initialized".to_string()))
|
.complete_component(
|
||||||
|
COMPONENT_TRACING_INIT,
|
||||||
|
Some("tracing initialized".to_string()),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
info!("Telemt MTProxy v{}", env!("CARGO_PKG_VERSION"));
|
info!("Telemt MTProxy v{}", env!("CARGO_PKG_VERSION"));
|
||||||
@@ -199,6 +393,7 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
config.general.upstream_connect_retry_attempts,
|
config.general.upstream_connect_retry_attempts,
|
||||||
config.general.upstream_connect_retry_backoff_ms,
|
config.general.upstream_connect_retry_backoff_ms,
|
||||||
config.general.upstream_connect_budget_ms,
|
config.general.upstream_connect_budget_ms,
|
||||||
|
config.general.tg_connect,
|
||||||
config.general.upstream_unhealthy_fail_threshold,
|
config.general.upstream_unhealthy_fail_threshold,
|
||||||
config.general.upstream_connect_failfast_hard_errors,
|
config.general.upstream_connect_failfast_hard_errors,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
@@ -216,7 +411,8 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
config.access.user_max_unique_ips_window_secs,
|
config.access.user_max_unique_ips_window_secs,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if config.access.user_max_unique_ips_global_each > 0 || !config.access.user_max_unique_ips.is_empty()
|
if config.access.user_max_unique_ips_global_each > 0
|
||||||
|
|| !config.access.user_max_unique_ips.is_empty()
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
global_each_limit = config.access.user_max_unique_ips_global_each,
|
global_each_limit = config.access.user_max_unique_ips_global_each,
|
||||||
@@ -243,7 +439,10 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
let route_runtime = Arc::new(RouteRuntimeController::new(initial_route_mode));
|
let route_runtime = Arc::new(RouteRuntimeController::new(initial_route_mode));
|
||||||
let api_me_pool = Arc::new(RwLock::new(None::<Arc<MePool>>));
|
let api_me_pool = Arc::new(RwLock::new(None::<Arc<MePool>>));
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(COMPONENT_API_BOOTSTRAP, Some("spawn API listener task".to_string()))
|
.start_component(
|
||||||
|
COMPONENT_API_BOOTSTRAP,
|
||||||
|
Some("spawn API listener task".to_string()),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if config.server.api.enabled {
|
if config.server.api.enabled {
|
||||||
@@ -326,7 +525,10 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(COMPONENT_NETWORK_PROBE, Some("probe network capabilities".to_string()))
|
.start_component(
|
||||||
|
COMPONENT_NETWORK_PROBE,
|
||||||
|
Some("probe network capabilities".to_string()),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
let probe = run_probe(
|
let probe = run_probe(
|
||||||
&config.network,
|
&config.network,
|
||||||
@@ -339,11 +541,8 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
probe.detected_ipv4.map(IpAddr::V4),
|
probe.detected_ipv4.map(IpAddr::V4),
|
||||||
probe.detected_ipv6.map(IpAddr::V6),
|
probe.detected_ipv6.map(IpAddr::V6),
|
||||||
));
|
));
|
||||||
let decision = decide_network_capabilities(
|
let decision =
|
||||||
&config.network,
|
decide_network_capabilities(&config.network, &probe, config.general.middle_proxy_nat_ip);
|
||||||
&probe,
|
|
||||||
config.general.middle_proxy_nat_ip,
|
|
||||||
);
|
|
||||||
log_probe_result(&probe, &decision);
|
log_probe_result(&probe, &decision);
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.complete_component(
|
.complete_component(
|
||||||
@@ -446,24 +645,16 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// If ME failed to initialize, force direct-only mode.
|
// If ME failed to initialize, force direct-only mode.
|
||||||
if me_pool.is_some() {
|
if me_pool.is_some() {
|
||||||
startup_tracker
|
startup_tracker.set_transport_mode("middle_proxy").await;
|
||||||
.set_transport_mode("middle_proxy")
|
startup_tracker.set_degraded(false).await;
|
||||||
.await;
|
|
||||||
startup_tracker
|
|
||||||
.set_degraded(false)
|
|
||||||
.await;
|
|
||||||
info!("Transport: Middle-End Proxy - all DC-over-RPC");
|
info!("Transport: Middle-End Proxy - all DC-over-RPC");
|
||||||
} else {
|
} else {
|
||||||
let _ = use_middle_proxy;
|
let _ = use_middle_proxy;
|
||||||
use_middle_proxy = false;
|
use_middle_proxy = false;
|
||||||
// Make runtime config reflect direct-only mode for handlers.
|
// Make runtime config reflect direct-only mode for handlers.
|
||||||
config.general.use_middle_proxy = false;
|
config.general.use_middle_proxy = false;
|
||||||
startup_tracker
|
startup_tracker.set_transport_mode("direct").await;
|
||||||
.set_transport_mode("direct")
|
startup_tracker.set_degraded(true).await;
|
||||||
.await;
|
|
||||||
startup_tracker
|
|
||||||
.set_degraded(true)
|
|
||||||
.await;
|
|
||||||
if me2dc_fallback {
|
if me2dc_fallback {
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.set_me_status(StartupMeStatus::Failed, "fallback_to_direct")
|
.set_me_status(StartupMeStatus::Failed, "fallback_to_direct")
|
||||||
@@ -484,7 +675,7 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
Duration::from_secs(config.access.replay_window_secs),
|
Duration::from_secs(config.access.replay_window_secs),
|
||||||
));
|
));
|
||||||
|
|
||||||
let buffer_pool = Arc::new(BufferPool::with_config(16 * 1024, 4096));
|
let buffer_pool = Arc::new(BufferPool::with_config(64 * 1024, 4096));
|
||||||
|
|
||||||
connectivity::run_startup_connectivity(
|
connectivity::run_startup_connectivity(
|
||||||
&config,
|
&config,
|
||||||
@@ -532,6 +723,12 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let _admission_tx_hold = admission_tx;
|
let _admission_tx_hold = admission_tx;
|
||||||
|
let shared_state = ProxySharedState::new();
|
||||||
|
conntrack_control::spawn_conntrack_controller(
|
||||||
|
config_rx.clone(),
|
||||||
|
stats.clone(),
|
||||||
|
shared_state.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let bound = listeners::bind_listeners(
|
let bound = listeners::bind_listeners(
|
||||||
&config,
|
&config,
|
||||||
@@ -552,6 +749,7 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
tls_cache.clone(),
|
tls_cache.clone(),
|
||||||
ip_tracker.clone(),
|
ip_tracker.clone(),
|
||||||
beobachten.clone(),
|
beobachten.clone(),
|
||||||
|
shared_state.clone(),
|
||||||
max_connections.clone(),
|
max_connections.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -563,6 +761,14 @@ 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,
|
||||||
@@ -583,6 +789,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(),
|
||||||
@@ -597,10 +806,11 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
tls_cache.clone(),
|
tls_cache.clone(),
|
||||||
ip_tracker.clone(),
|
ip_tracker.clone(),
|
||||||
beobachten.clone(),
|
beobachten.clone(),
|
||||||
|
shared_state,
|
||||||
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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,24 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use tokio::sync::{mpsc, watch};
|
use tokio::sync::{mpsc, watch};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use tracing_subscriber::reload;
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use tracing_subscriber::reload;
|
||||||
|
|
||||||
use crate::config::{LogLevel, ProxyConfig};
|
|
||||||
use crate::config::hot_reload::spawn_config_watcher;
|
use crate::config::hot_reload::spawn_config_watcher;
|
||||||
|
use crate::config::{LogLevel, ProxyConfig};
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::metrics;
|
use crate::metrics;
|
||||||
use crate::network::probe::NetworkProbe;
|
use crate::network::probe::NetworkProbe;
|
||||||
use crate::startup::{COMPONENT_CONFIG_WATCHER_START, COMPONENT_METRICS_START, COMPONENT_RUNTIME_READY, StartupTracker};
|
use crate::startup::{
|
||||||
|
COMPONENT_CONFIG_WATCHER_START, COMPONENT_METRICS_START, COMPONENT_RUNTIME_READY,
|
||||||
|
StartupTracker,
|
||||||
|
};
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::stats::telemetry::TelemetryPolicy;
|
use crate::stats::telemetry::TelemetryPolicy;
|
||||||
use crate::stats::{ReplayChecker, Stats};
|
use crate::stats::{ReplayChecker, Stats};
|
||||||
use crate::transport::middle_proxy::{MePool, MeReinitTrigger};
|
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
use crate::transport::middle_proxy::{MePool, MeReinitTrigger};
|
||||||
|
|
||||||
use super::helpers::write_beobachten_snapshot;
|
use super::helpers::write_beobachten_snapshot;
|
||||||
|
|
||||||
@@ -79,15 +82,13 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
Some("spawn config hot-reload watcher".to_string()),
|
Some("spawn config hot-reload watcher".to_string()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (config_rx, log_level_rx): (
|
let (config_rx, log_level_rx): (watch::Receiver<Arc<ProxyConfig>>, watch::Receiver<LogLevel>) =
|
||||||
watch::Receiver<Arc<ProxyConfig>>,
|
spawn_config_watcher(
|
||||||
watch::Receiver<LogLevel>,
|
config_path.to_path_buf(),
|
||||||
) = spawn_config_watcher(
|
config.clone(),
|
||||||
config_path.to_path_buf(),
|
detected_ip_v4,
|
||||||
config.clone(),
|
detected_ip_v6,
|
||||||
detected_ip_v4,
|
);
|
||||||
detected_ip_v6,
|
|
||||||
);
|
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.complete_component(
|
.complete_component(
|
||||||
COMPONENT_CONFIG_WATCHER_START,
|
COMPONENT_CONFIG_WATCHER_START,
|
||||||
@@ -114,7 +115,8 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let cfg = config_rx_policy.borrow_and_update().clone();
|
let cfg = config_rx_policy.borrow_and_update().clone();
|
||||||
stats_policy.apply_telemetry_policy(TelemetryPolicy::from_config(&cfg.general.telemetry));
|
stats_policy
|
||||||
|
.apply_telemetry_policy(TelemetryPolicy::from_config(&cfg.general.telemetry));
|
||||||
if let Some(pool) = &me_pool_for_policy {
|
if let Some(pool) = &me_pool_for_policy {
|
||||||
pool.update_runtime_transport_policy(
|
pool.update_runtime_transport_policy(
|
||||||
cfg.general.me_socks_kdf_policy,
|
cfg.general.me_socks_kdf_policy,
|
||||||
@@ -130,7 +132,11 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
let ip_tracker_policy = ip_tracker.clone();
|
let ip_tracker_policy = ip_tracker.clone();
|
||||||
let mut config_rx_ip_limits = config_rx.clone();
|
let mut config_rx_ip_limits = config_rx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut prev_limits = config_rx_ip_limits.borrow().access.user_max_unique_ips.clone();
|
let mut prev_limits = config_rx_ip_limits
|
||||||
|
.borrow()
|
||||||
|
.access
|
||||||
|
.user_max_unique_ips
|
||||||
|
.clone();
|
||||||
let mut prev_global_each = config_rx_ip_limits
|
let mut prev_global_each = config_rx_ip_limits
|
||||||
.borrow()
|
.borrow()
|
||||||
.access
|
.access
|
||||||
@@ -183,7 +189,9 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
let sleep_secs = cfg.general.beobachten_flush_secs.max(1);
|
let sleep_secs = cfg.general.beobachten_flush_secs.max(1);
|
||||||
|
|
||||||
if cfg.general.beobachten {
|
if cfg.general.beobachten {
|
||||||
let ttl = std::time::Duration::from_secs(cfg.general.beobachten_minutes.saturating_mul(60));
|
let ttl = std::time::Duration::from_secs(
|
||||||
|
cfg.general.beobachten_minutes.saturating_mul(60),
|
||||||
|
);
|
||||||
let path = cfg.general.beobachten_file.clone();
|
let path = cfg.general.beobachten_file.clone();
|
||||||
let snapshot = beobachten_writer.snapshot_text(ttl);
|
let snapshot = beobachten_writer.snapshot_text(ttl);
|
||||||
if let Err(e) = write_beobachten_snapshot(&path, &snapshot).await {
|
if let Err(e) = write_beobachten_snapshot(&path, &snapshot).await {
|
||||||
@@ -227,8 +235,11 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
let config_rx_clone_rot = config_rx.clone();
|
let config_rx_clone_rot = config_rx.clone();
|
||||||
let reinit_tx_rotation = reinit_tx.clone();
|
let reinit_tx_rotation = reinit_tx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
crate::transport::middle_proxy::me_rotation_task(config_rx_clone_rot, reinit_tx_rotation)
|
crate::transport::middle_proxy::me_rotation_task(
|
||||||
.await;
|
config_rx_clone_rot,
|
||||||
|
reinit_tx_rotation,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,10 +323,12 @@ pub(crate) async fn spawn_metrics_if_configured(
|
|||||||
let config_rx_metrics = config_rx.clone();
|
let config_rx_metrics = config_rx.clone();
|
||||||
let ip_tracker_metrics = ip_tracker.clone();
|
let ip_tracker_metrics = ip_tracker.clone();
|
||||||
let whitelist = config.server.metrics_whitelist.clone();
|
let whitelist = config.server.metrics_whitelist.clone();
|
||||||
|
let listen_backlog = config.server.listen_backlog;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
metrics::serve(
|
metrics::serve(
|
||||||
port,
|
port,
|
||||||
listen,
|
listen,
|
||||||
|
listen_backlog,
|
||||||
stats,
|
stats,
|
||||||
beobachten,
|
beobachten,
|
||||||
ip_tracker_metrics,
|
ip_tracker_metrics,
|
||||||
|
|||||||
+194
-30
@@ -1,42 +1,206 @@
|
|||||||
|
//! 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(not(unix))]
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tracing::{error, info, warn};
|
#[cfg(unix)]
|
||||||
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
|
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(Duration::from_secs(2), pool.shutdown_send_close_conn_all())
|
Quit,
|
||||||
.await
|
}
|
||||||
{
|
|
||||||
Ok(total) => {
|
impl std::fmt::Display for ShutdownSignal {
|
||||||
info!(
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
close_conn_sent = total,
|
match self {
|
||||||
"ME shutdown: RPC_CLOSE_CONN broadcast completed"
|
ShutdownSignal::Interrupt => write!(f, "SIGINT"),
|
||||||
);
|
ShutdownSignal::Terminate => write!(f, "SIGTERM"),
|
||||||
}
|
ShutdownSignal::Quit => write!(f, "SIGQUIT"),
|
||||||
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use rand::Rng;
|
use rand::RngExt;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::startup::{COMPONENT_TLS_FRONT_BOOTSTRAP, StartupTracker};
|
use crate::startup::{COMPONENT_TLS_FRONT_BOOTSTRAP, StartupTracker};
|
||||||
use crate::tls_front::TlsFrontCache;
|
use crate::tls_front::TlsFrontCache;
|
||||||
|
use crate::tls_front::fetcher::TlsFetchStrategy;
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
pub(crate) async fn bootstrap_tls_front(
|
pub(crate) async fn bootstrap_tls_front(
|
||||||
@@ -38,27 +39,44 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| config.censorship.tls_domain.clone());
|
.unwrap_or_else(|| config.censorship.tls_domain.clone());
|
||||||
let mask_unix_sock = config.censorship.mask_unix_sock.clone();
|
let mask_unix_sock = config.censorship.mask_unix_sock.clone();
|
||||||
let fetch_timeout = Duration::from_secs(5);
|
let tls_fetch_scope = (!config.censorship.tls_fetch_scope.is_empty())
|
||||||
|
.then(|| config.censorship.tls_fetch_scope.clone());
|
||||||
|
let tls_fetch = config.censorship.tls_fetch.clone();
|
||||||
|
let fetch_strategy = TlsFetchStrategy {
|
||||||
|
profiles: tls_fetch.profiles,
|
||||||
|
strict_route: tls_fetch.strict_route,
|
||||||
|
attempt_timeout: Duration::from_millis(tls_fetch.attempt_timeout_ms.max(1)),
|
||||||
|
total_budget: Duration::from_millis(tls_fetch.total_budget_ms.max(1)),
|
||||||
|
grease_enabled: tls_fetch.grease_enabled,
|
||||||
|
deterministic: tls_fetch.deterministic,
|
||||||
|
profile_cache_ttl: Duration::from_secs(tls_fetch.profile_cache_ttl_secs),
|
||||||
|
};
|
||||||
|
let fetch_timeout = fetch_strategy.total_budget;
|
||||||
|
|
||||||
let cache_initial = cache.clone();
|
let cache_initial = cache.clone();
|
||||||
let domains_initial = tls_domains.to_vec();
|
let domains_initial = tls_domains.to_vec();
|
||||||
let host_initial = mask_host.clone();
|
let host_initial = mask_host.clone();
|
||||||
let unix_sock_initial = mask_unix_sock.clone();
|
let unix_sock_initial = mask_unix_sock.clone();
|
||||||
|
let scope_initial = tls_fetch_scope.clone();
|
||||||
let upstream_initial = upstream_manager.clone();
|
let upstream_initial = upstream_manager.clone();
|
||||||
|
let strategy_initial = fetch_strategy.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut join = tokio::task::JoinSet::new();
|
let mut join = tokio::task::JoinSet::new();
|
||||||
for domain in domains_initial {
|
for domain in domains_initial {
|
||||||
let cache_domain = cache_initial.clone();
|
let cache_domain = cache_initial.clone();
|
||||||
let host_domain = host_initial.clone();
|
let host_domain = host_initial.clone();
|
||||||
let unix_sock_domain = unix_sock_initial.clone();
|
let unix_sock_domain = unix_sock_initial.clone();
|
||||||
|
let scope_domain = scope_initial.clone();
|
||||||
let upstream_domain = upstream_initial.clone();
|
let upstream_domain = upstream_initial.clone();
|
||||||
|
let strategy_domain = strategy_initial.clone();
|
||||||
join.spawn(async move {
|
join.spawn(async move {
|
||||||
match crate::tls_front::fetcher::fetch_real_tls(
|
match crate::tls_front::fetcher::fetch_real_tls_with_strategy(
|
||||||
&host_domain,
|
&host_domain,
|
||||||
port,
|
port,
|
||||||
&domain,
|
&domain,
|
||||||
fetch_timeout,
|
&strategy_domain,
|
||||||
Some(upstream_domain),
|
Some(upstream_domain),
|
||||||
|
scope_domain.as_deref(),
|
||||||
proxy_protocol,
|
proxy_protocol,
|
||||||
unix_sock_domain.as_deref(),
|
unix_sock_domain.as_deref(),
|
||||||
)
|
)
|
||||||
@@ -100,7 +118,9 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let domains_refresh = tls_domains.to_vec();
|
let domains_refresh = tls_domains.to_vec();
|
||||||
let host_refresh = mask_host.clone();
|
let host_refresh = mask_host.clone();
|
||||||
let unix_sock_refresh = mask_unix_sock.clone();
|
let unix_sock_refresh = mask_unix_sock.clone();
|
||||||
|
let scope_refresh = tls_fetch_scope.clone();
|
||||||
let upstream_refresh = upstream_manager.clone();
|
let upstream_refresh = upstream_manager.clone();
|
||||||
|
let strategy_refresh = fetch_strategy.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let base_secs = rand::rng().random_range(4 * 3600..=6 * 3600);
|
let base_secs = rand::rng().random_range(4 * 3600..=6 * 3600);
|
||||||
@@ -112,14 +132,17 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let cache_domain = cache_refresh.clone();
|
let cache_domain = cache_refresh.clone();
|
||||||
let host_domain = host_refresh.clone();
|
let host_domain = host_refresh.clone();
|
||||||
let unix_sock_domain = unix_sock_refresh.clone();
|
let unix_sock_domain = unix_sock_refresh.clone();
|
||||||
|
let scope_domain = scope_refresh.clone();
|
||||||
let upstream_domain = upstream_refresh.clone();
|
let upstream_domain = upstream_refresh.clone();
|
||||||
|
let strategy_domain = strategy_refresh.clone();
|
||||||
join.spawn(async move {
|
join.spawn(async move {
|
||||||
match crate::tls_front::fetcher::fetch_real_tls(
|
match crate::tls_front::fetcher::fetch_real_tls_with_strategy(
|
||||||
&host_domain,
|
&host_domain,
|
||||||
port,
|
port,
|
||||||
&domain,
|
&domain,
|
||||||
fetch_timeout,
|
&strategy_domain,
|
||||||
Some(upstream_domain),
|
Some(upstream_domain),
|
||||||
|
scope_domain.as_deref(),
|
||||||
proxy_protocol,
|
proxy_protocol,
|
||||||
unix_sock_domain.as_deref(),
|
unix_sock_domain.as_deref(),
|
||||||
)
|
)
|
||||||
|
|||||||
+57
-3
@@ -3,16 +3,28 @@
|
|||||||
mod api;
|
mod api;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod conntrack_control;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod daemon;
|
||||||
mod error;
|
mod error;
|
||||||
mod ip_tracker;
|
mod ip_tracker;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[path = "tests/ip_tracker_encapsulation_adversarial_tests.rs"]
|
||||||
|
mod ip_tracker_encapsulation_adversarial_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/ip_tracker_hotpath_adversarial_tests.rs"]
|
||||||
|
mod ip_tracker_hotpath_adversarial_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/ip_tracker_regression_tests.rs"]
|
||||||
mod ip_tracker_regression_tests;
|
mod ip_tracker_regression_tests;
|
||||||
|
mod logging;
|
||||||
mod maestro;
|
mod maestro;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod network;
|
mod network;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
mod proxy;
|
mod proxy;
|
||||||
|
mod service;
|
||||||
mod startup;
|
mod startup;
|
||||||
mod stats;
|
mod stats;
|
||||||
mod stream;
|
mod stream;
|
||||||
@@ -20,7 +32,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>> {
|
// Install rustls crypto provider early
|
||||||
maestro::run().await
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let daemon_opts = cmd.daemon_opts;
|
||||||
|
|
||||||
|
// Daemonize BEFORE runtime
|
||||||
|
if daemon_opts.should_daemonize() {
|
||||||
|
match daemon::daemonize(daemon_opts.working_dir.as_deref()) {
|
||||||
|
Ok(daemon::DaemonizeResult::Parent) => {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Ok(daemon::DaemonizeResult::Child) => {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[telemt] Daemonization failed: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1156
-112
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,7 @@ fn parse_ip_spec(ip_spec: &str) -> Result<IpAddr> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ip = ip_spec.parse::<IpAddr>().map_err(|_| {
|
let ip = ip_spec.parse::<IpAddr>().map_err(|_| {
|
||||||
ProxyError::Config(format!(
|
ProxyError::Config(format!("network.dns_overrides IP is invalid: '{ip_spec}'"))
|
||||||
"network.dns_overrides IP is invalid: '{ip_spec}'"
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
if matches!(ip, IpAddr::V6(_)) {
|
if matches!(ip, IpAddr::V6(_)) {
|
||||||
return Err(ProxyError::Config(format!(
|
return Err(ProxyError::Config(format!(
|
||||||
@@ -103,9 +101,9 @@ pub fn validate_entries(entries: &[String]) -> Result<()> {
|
|||||||
/// Replace runtime DNS overrides with a new validated snapshot.
|
/// Replace runtime DNS overrides with a new validated snapshot.
|
||||||
pub fn install_entries(entries: &[String]) -> Result<()> {
|
pub fn install_entries(entries: &[String]) -> Result<()> {
|
||||||
let parsed = parse_entries(entries)?;
|
let parsed = parse_entries(entries)?;
|
||||||
let mut guard = overrides_store()
|
let mut guard = overrides_store().write().map_err(|_| {
|
||||||
.write()
|
ProxyError::Config("network.dns_overrides runtime lock is poisoned".to_string())
|
||||||
.map_err(|_| ProxyError::Config("network.dns_overrides runtime lock is poisoned".to_string()))?;
|
})?;
|
||||||
*guard = parsed;
|
*guard = parsed;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-32
@@ -1,4 +1,5 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
#![allow(clippy::items_after_test_module)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
|
||||||
@@ -10,7 +11,9 @@ use tracing::{debug, info, warn};
|
|||||||
|
|
||||||
use crate::config::{NetworkConfig, UpstreamConfig, UpstreamType};
|
use crate::config::{NetworkConfig, UpstreamConfig, UpstreamType};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::network::stun::{stun_probe_family_with_bind, DualStunResult, IpFamily, StunProbeResult};
|
use crate::network::stun::{
|
||||||
|
DualStunResult, IpFamily, StunProbeResult, stun_probe_family_with_bind,
|
||||||
|
};
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -78,13 +81,8 @@ pub async fn run_probe(
|
|||||||
warn!("STUN probe is enabled but network.stun_servers is empty");
|
warn!("STUN probe is enabled but network.stun_servers is empty");
|
||||||
DualStunResult::default()
|
DualStunResult::default()
|
||||||
} else {
|
} else {
|
||||||
probe_stun_servers_parallel(
|
probe_stun_servers_parallel(&servers, stun_nat_probe_concurrency.max(1), None, None)
|
||||||
&servers,
|
.await
|
||||||
stun_nat_probe_concurrency.max(1),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
} else if nat_probe {
|
} else if nat_probe {
|
||||||
info!("STUN probe is disabled by network.stun_use=false");
|
info!("STUN probe is disabled by network.stun_use=false");
|
||||||
@@ -99,7 +97,8 @@ pub async fn run_probe(
|
|||||||
let UpstreamType::Direct {
|
let UpstreamType::Direct {
|
||||||
interface,
|
interface,
|
||||||
bind_addresses,
|
bind_addresses,
|
||||||
} = &upstream.upstream_type else {
|
} = &upstream.upstream_type
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Some(addrs) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
|
if let Some(addrs) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
|
||||||
@@ -199,11 +198,10 @@ pub async fn run_probe(
|
|||||||
if nat_probe
|
if nat_probe
|
||||||
&& probe.reflected_ipv4.is_none()
|
&& probe.reflected_ipv4.is_none()
|
||||||
&& probe.detected_ipv4.map(is_bogon_v4).unwrap_or(false)
|
&& probe.detected_ipv4.map(is_bogon_v4).unwrap_or(false)
|
||||||
|
&& let Some(public_ip) = detect_public_ipv4_http(&config.http_ip_detect_urls).await
|
||||||
{
|
{
|
||||||
if let Some(public_ip) = detect_public_ipv4_http(&config.http_ip_detect_urls).await {
|
probe.reflected_ipv4 = Some(SocketAddr::new(IpAddr::V4(public_ip), 0));
|
||||||
probe.reflected_ipv4 = Some(SocketAddr::new(IpAddr::V4(public_ip), 0));
|
info!(public_ip = %public_ip, "STUN unavailable, using HTTP public IPv4 fallback");
|
||||||
info!(public_ip = %public_ip, "STUN unavailable, using HTTP public IPv4 fallback");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
probe.ipv4_nat_detected = match (probe.detected_ipv4, probe.reflected_ipv4) {
|
probe.ipv4_nat_detected = match (probe.detected_ipv4, probe.reflected_ipv4) {
|
||||||
@@ -217,12 +215,20 @@ pub async fn run_probe(
|
|||||||
|
|
||||||
probe.ipv4_usable = config.ipv4
|
probe.ipv4_usable = config.ipv4
|
||||||
&& probe.detected_ipv4.is_some()
|
&& probe.detected_ipv4.is_some()
|
||||||
&& (!probe.ipv4_is_bogon || probe.reflected_ipv4.map(|r| !is_bogon(r.ip())).unwrap_or(false));
|
&& (!probe.ipv4_is_bogon
|
||||||
|
|| probe
|
||||||
|
.reflected_ipv4
|
||||||
|
.map(|r| !is_bogon(r.ip()))
|
||||||
|
.unwrap_or(false));
|
||||||
|
|
||||||
let ipv6_enabled = config.ipv6.unwrap_or(probe.detected_ipv6.is_some());
|
let ipv6_enabled = config.ipv6.unwrap_or(probe.detected_ipv6.is_some());
|
||||||
probe.ipv6_usable = ipv6_enabled
|
probe.ipv6_usable = ipv6_enabled
|
||||||
&& probe.detected_ipv6.is_some()
|
&& probe.detected_ipv6.is_some()
|
||||||
&& (!probe.ipv6_is_bogon || probe.reflected_ipv6.map(|r| !is_bogon(r.ip())).unwrap_or(false));
|
&& (!probe.ipv6_is_bogon
|
||||||
|
|| probe
|
||||||
|
.reflected_ipv6
|
||||||
|
.map(|r| !is_bogon(r.ip()))
|
||||||
|
.unwrap_or(false));
|
||||||
|
|
||||||
Ok(probe)
|
Ok(probe)
|
||||||
}
|
}
|
||||||
@@ -280,8 +286,6 @@ async fn probe_stun_servers_parallel(
|
|||||||
while next_idx < servers.len() && join_set.len() < concurrency {
|
while next_idx < servers.len() && join_set.len() < concurrency {
|
||||||
let stun_addr = servers[next_idx].clone();
|
let stun_addr = servers[next_idx].clone();
|
||||||
next_idx += 1;
|
next_idx += 1;
|
||||||
let bind_v4 = bind_v4;
|
|
||||||
let bind_v6 = bind_v6;
|
|
||||||
join_set.spawn(async move {
|
join_set.spawn(async move {
|
||||||
let res = timeout(STUN_BATCH_TIMEOUT, async {
|
let res = timeout(STUN_BATCH_TIMEOUT, async {
|
||||||
let v4 = stun_probe_family_with_bind(&stun_addr, IpFamily::V4, bind_v4).await?;
|
let v4 = stun_probe_family_with_bind(&stun_addr, IpFamily::V4, bind_v4).await?;
|
||||||
@@ -300,11 +304,15 @@ async fn probe_stun_servers_parallel(
|
|||||||
match task {
|
match task {
|
||||||
Ok((stun_addr, Ok(Ok(result)))) => {
|
Ok((stun_addr, Ok(Ok(result)))) => {
|
||||||
if let Some(v4) = result.v4 {
|
if let Some(v4) = result.v4 {
|
||||||
let entry = best_v4_by_ip.entry(v4.reflected_addr.ip()).or_insert((0, v4));
|
let entry = best_v4_by_ip
|
||||||
|
.entry(v4.reflected_addr.ip())
|
||||||
|
.or_insert((0, v4));
|
||||||
entry.0 += 1;
|
entry.0 += 1;
|
||||||
}
|
}
|
||||||
if let Some(v6) = result.v6 {
|
if let Some(v6) = result.v6 {
|
||||||
let entry = best_v6_by_ip.entry(v6.reflected_addr.ip()).or_insert((0, v6));
|
let entry = best_v6_by_ip
|
||||||
|
.entry(v6.reflected_addr.ip())
|
||||||
|
.or_insert((0, v6));
|
||||||
entry.0 += 1;
|
entry.0 += 1;
|
||||||
}
|
}
|
||||||
if result.v4.is_some() || result.v6.is_some() {
|
if result.v4.is_some() || result.v6.is_some() {
|
||||||
@@ -324,17 +332,11 @@ async fn probe_stun_servers_parallel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut out = DualStunResult::default();
|
let mut out = DualStunResult::default();
|
||||||
if let Some((_, best)) = best_v4_by_ip
|
if let Some((_, best)) = best_v4_by_ip.into_values().max_by_key(|(count, _)| *count) {
|
||||||
.into_values()
|
|
||||||
.max_by_key(|(count, _)| *count)
|
|
||||||
{
|
|
||||||
info!("STUN-Quorum reached, IP: {}", best.reflected_addr.ip());
|
info!("STUN-Quorum reached, IP: {}", best.reflected_addr.ip());
|
||||||
out.v4 = Some(best);
|
out.v4 = Some(best);
|
||||||
}
|
}
|
||||||
if let Some((_, best)) = best_v6_by_ip
|
if let Some((_, best)) = best_v6_by_ip.into_values().max_by_key(|(count, _)| *count) {
|
||||||
.into_values()
|
|
||||||
.max_by_key(|(count, _)| *count)
|
|
||||||
{
|
|
||||||
info!("STUN-Quorum reached, IP: {}", best.reflected_addr.ip());
|
info!("STUN-Quorum reached, IP: {}", best.reflected_addr.ip());
|
||||||
out.v6 = Some(best);
|
out.v6 = Some(best);
|
||||||
}
|
}
|
||||||
@@ -347,7 +349,8 @@ pub fn decide_network_capabilities(
|
|||||||
middle_proxy_nat_ip: Option<IpAddr>,
|
middle_proxy_nat_ip: Option<IpAddr>,
|
||||||
) -> NetworkDecision {
|
) -> NetworkDecision {
|
||||||
let ipv4_dc = config.ipv4 && probe.detected_ipv4.is_some();
|
let ipv4_dc = config.ipv4 && probe.detected_ipv4.is_some();
|
||||||
let ipv6_dc = config.ipv6.unwrap_or(probe.detected_ipv6.is_some()) && probe.detected_ipv6.is_some();
|
let ipv6_dc =
|
||||||
|
config.ipv6.unwrap_or(probe.detected_ipv6.is_some()) && probe.detected_ipv6.is_some();
|
||||||
let nat_ip_v4 = matches!(middle_proxy_nat_ip, Some(IpAddr::V4(_)));
|
let nat_ip_v4 = matches!(middle_proxy_nat_ip, Some(IpAddr::V4(_)));
|
||||||
let nat_ip_v6 = matches!(middle_proxy_nat_ip, Some(IpAddr::V6(_)));
|
let nat_ip_v6 = matches!(middle_proxy_nat_ip, Some(IpAddr::V6(_)));
|
||||||
|
|
||||||
@@ -534,10 +537,26 @@ pub fn is_bogon_v6(ip: Ipv6Addr) -> bool {
|
|||||||
|
|
||||||
pub fn log_probe_result(probe: &NetworkProbe, decision: &NetworkDecision) {
|
pub fn log_probe_result(probe: &NetworkProbe, decision: &NetworkDecision) {
|
||||||
info!(
|
info!(
|
||||||
ipv4 = probe.detected_ipv4.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
|
ipv4 = probe
|
||||||
ipv6 = probe.detected_ipv6.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
|
.detected_ipv4
|
||||||
reflected_v4 = probe.reflected_ipv4.as_ref().map(|v| v.ip().to_string()).unwrap_or_else(|| "-".into()),
|
.as_ref()
|
||||||
reflected_v6 = probe.reflected_ipv6.as_ref().map(|v| v.ip().to_string()).unwrap_or_else(|| "-".into()),
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_else(|| "-".into()),
|
||||||
|
ipv6 = probe
|
||||||
|
.detected_ipv6
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_else(|| "-".into()),
|
||||||
|
reflected_v4 = probe
|
||||||
|
.reflected_ipv4
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.ip().to_string())
|
||||||
|
.unwrap_or_else(|| "-".into()),
|
||||||
|
reflected_v6 = probe
|
||||||
|
.reflected_ipv6
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.ip().to_string())
|
||||||
|
.unwrap_or_else(|| "-".into()),
|
||||||
ipv4_bogon = probe.ipv4_is_bogon,
|
ipv4_bogon = probe.ipv4_is_bogon,
|
||||||
ipv6_bogon = probe.ipv6_is_bogon,
|
ipv6_bogon = probe.ipv6_is_bogon,
|
||||||
ipv4_me = decision.ipv4_me,
|
ipv4_me = decision.ipv4_me,
|
||||||
|
|||||||
+44
-31
@@ -2,13 +2,20 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use tokio::net::{lookup_host, UdpSocket};
|
use tokio::net::{UdpSocket, lookup_host};
|
||||||
use tokio::time::{timeout, Duration, sleep};
|
use tokio::time::{Duration, sleep, timeout};
|
||||||
|
|
||||||
|
use crate::crypto::SecureRandom;
|
||||||
use crate::error::{ProxyError, Result};
|
use crate::error::{ProxyError, Result};
|
||||||
use crate::network::dns_overrides::{resolve, split_host_port};
|
use crate::network::dns_overrides::{resolve, split_host_port};
|
||||||
|
|
||||||
|
fn stun_rng() -> &'static SecureRandom {
|
||||||
|
static STUN_RNG: OnceLock<SecureRandom> = OnceLock::new();
|
||||||
|
STUN_RNG.get_or_init(SecureRandom::new)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum IpFamily {
|
pub enum IpFamily {
|
||||||
V4,
|
V4,
|
||||||
@@ -34,13 +41,13 @@ pub async fn stun_probe_dual(stun_addr: &str) -> Result<DualStunResult> {
|
|||||||
stun_probe_family(stun_addr, IpFamily::V6),
|
stun_probe_family(stun_addr, IpFamily::V6),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(DualStunResult {
|
Ok(DualStunResult { v4: v4?, v6: v6? })
|
||||||
v4: v4?,
|
|
||||||
v6: v6?,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stun_probe_family(stun_addr: &str, family: IpFamily) -> Result<Option<StunProbeResult>> {
|
pub async fn stun_probe_family(
|
||||||
|
stun_addr: &str,
|
||||||
|
family: IpFamily,
|
||||||
|
) -> Result<Option<StunProbeResult>> {
|
||||||
stun_probe_family_with_bind(stun_addr, family, None).await
|
stun_probe_family_with_bind(stun_addr, family, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +56,6 @@ pub async fn stun_probe_family_with_bind(
|
|||||||
family: IpFamily,
|
family: IpFamily,
|
||||||
bind_ip: Option<IpAddr>,
|
bind_ip: Option<IpAddr>,
|
||||||
) -> Result<Option<StunProbeResult>> {
|
) -> Result<Option<StunProbeResult>> {
|
||||||
use rand::RngCore;
|
|
||||||
|
|
||||||
let bind_addr = match (family, bind_ip) {
|
let bind_addr = match (family, bind_ip) {
|
||||||
(IpFamily::V4, Some(IpAddr::V4(ip))) => SocketAddr::new(IpAddr::V4(ip), 0),
|
(IpFamily::V4, Some(IpAddr::V4(ip))) => SocketAddr::new(IpAddr::V4(ip), 0),
|
||||||
(IpFamily::V6, Some(IpAddr::V6(ip))) => SocketAddr::new(IpAddr::V6(ip), 0),
|
(IpFamily::V6, Some(IpAddr::V6(ip))) => SocketAddr::new(IpAddr::V6(ip), 0),
|
||||||
@@ -71,13 +76,18 @@ pub async fn stun_probe_family_with_bind(
|
|||||||
if let Some(addr) = target_addr {
|
if let Some(addr) = target_addr {
|
||||||
match socket.connect(addr).await {
|
match socket.connect(addr).await {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(e) if family == IpFamily::V6 && matches!(
|
Err(e)
|
||||||
e.kind(),
|
if family == IpFamily::V6
|
||||||
std::io::ErrorKind::NetworkUnreachable
|
&& matches!(
|
||||||
| std::io::ErrorKind::HostUnreachable
|
e.kind(),
|
||||||
| std::io::ErrorKind::Unsupported
|
std::io::ErrorKind::NetworkUnreachable
|
||||||
| std::io::ErrorKind::NetworkDown
|
| std::io::ErrorKind::HostUnreachable
|
||||||
) => return Ok(None),
|
| std::io::ErrorKind::Unsupported
|
||||||
|
| std::io::ErrorKind::NetworkDown
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
Err(e) => return Err(ProxyError::Proxy(format!("STUN connect failed: {e}"))),
|
Err(e) => return Err(ProxyError::Proxy(format!("STUN connect failed: {e}"))),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -88,7 +98,7 @@ pub async fn stun_probe_family_with_bind(
|
|||||||
req[0..2].copy_from_slice(&0x0001u16.to_be_bytes()); // Binding Request
|
req[0..2].copy_from_slice(&0x0001u16.to_be_bytes()); // Binding Request
|
||||||
req[2..4].copy_from_slice(&0u16.to_be_bytes()); // length
|
req[2..4].copy_from_slice(&0u16.to_be_bytes()); // length
|
||||||
req[4..8].copy_from_slice(&0x2112A442u32.to_be_bytes()); // magic cookie
|
req[4..8].copy_from_slice(&0x2112A442u32.to_be_bytes()); // magic cookie
|
||||||
rand::rng().fill_bytes(&mut req[8..20]); // transaction ID
|
stun_rng().fill(&mut req[8..20]); // transaction ID
|
||||||
|
|
||||||
let mut buf = [0u8; 256];
|
let mut buf = [0u8; 256];
|
||||||
let mut attempt = 0;
|
let mut attempt = 0;
|
||||||
@@ -120,16 +130,16 @@ pub async fn stun_probe_family_with_bind(
|
|||||||
|
|
||||||
let magic = 0x2112A442u32.to_be_bytes();
|
let magic = 0x2112A442u32.to_be_bytes();
|
||||||
let txid = &req[8..20];
|
let txid = &req[8..20];
|
||||||
let mut idx = 20;
|
let mut idx = 20;
|
||||||
while idx + 4 <= n {
|
while idx + 4 <= n {
|
||||||
let atype = u16::from_be_bytes(buf[idx..idx + 2].try_into().unwrap());
|
let atype = u16::from_be_bytes(buf[idx..idx + 2].try_into().unwrap());
|
||||||
let alen = u16::from_be_bytes(buf[idx + 2..idx + 4].try_into().unwrap()) as usize;
|
let alen = u16::from_be_bytes(buf[idx + 2..idx + 4].try_into().unwrap()) as usize;
|
||||||
idx += 4;
|
idx += 4;
|
||||||
if idx + alen > n {
|
if idx + alen > n {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
match atype {
|
match atype {
|
||||||
0x0020 /* XOR-MAPPED-ADDRESS */ | 0x0001 /* MAPPED-ADDRESS */ => {
|
0x0020 /* XOR-MAPPED-ADDRESS */ | 0x0001 /* MAPPED-ADDRESS */ => {
|
||||||
if alen < 8 {
|
if alen < 8 {
|
||||||
break;
|
break;
|
||||||
@@ -198,9 +208,8 @@ pub async fn stun_probe_family_with_bind(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
idx += (alen + 3) & !3;
|
idx += (alen + 3) & !3;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -228,7 +237,11 @@ async fn resolve_stun_addr(stun_addr: &str, family: IpFamily) -> Result<Option<S
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| ProxyError::Proxy(format!("STUN resolve failed: {e}")))?;
|
.map_err(|e| ProxyError::Proxy(format!("STUN resolve failed: {e}")))?;
|
||||||
|
|
||||||
let target = addrs
|
let target = addrs.find(|a| {
|
||||||
.find(|a| matches!((a.is_ipv4(), family), (true, IpFamily::V4) | (false, IpFamily::V6)));
|
matches!(
|
||||||
|
(a.is_ipv4(), family),
|
||||||
|
(true, IpFamily::V4) | (false, IpFamily::V6)
|
||||||
|
)
|
||||||
|
});
|
||||||
Ok(target)
|
Ok(target)
|
||||||
}
|
}
|
||||||
|
|||||||
+151
-80
@@ -36,32 +36,86 @@ pub static TG_DATACENTERS_V6: LazyLock<Vec<IpAddr>> = LazyLock::new(|| {
|
|||||||
pub static TG_MIDDLE_PROXIES_V4: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
pub static TG_MIDDLE_PROXIES_V4: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
||||||
LazyLock::new(|| {
|
LazyLock::new(|| {
|
||||||
let mut m = std::collections::HashMap::new();
|
let mut m = std::collections::HashMap::new();
|
||||||
m.insert(1, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)]);
|
m.insert(
|
||||||
m.insert(-1, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)]);
|
1,
|
||||||
m.insert(2, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)]);
|
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)],
|
||||||
m.insert(-2, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)]);
|
);
|
||||||
m.insert(3, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)]);
|
m.insert(
|
||||||
m.insert(-3, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)]);
|
-1,
|
||||||
|
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
2,
|
||||||
|
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
-2,
|
||||||
|
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
3,
|
||||||
|
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
-3,
|
||||||
|
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)],
|
||||||
|
);
|
||||||
m.insert(4, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888)]);
|
m.insert(4, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888)]);
|
||||||
m.insert(-4, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 165, 109)), 8888)]);
|
m.insert(
|
||||||
|
-4,
|
||||||
|
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 165, 109)), 8888)],
|
||||||
|
);
|
||||||
m.insert(5, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)]);
|
m.insert(5, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)]);
|
||||||
m.insert(-5, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)]);
|
m.insert(
|
||||||
|
-5,
|
||||||
|
vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)],
|
||||||
|
);
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
|
|
||||||
pub static TG_MIDDLE_PROXIES_V6: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
pub static TG_MIDDLE_PROXIES_V6: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
||||||
LazyLock::new(|| {
|
LazyLock::new(|| {
|
||||||
let mut m = std::collections::HashMap::new();
|
let mut m = std::collections::HashMap::new();
|
||||||
m.insert(1, vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)]);
|
m.insert(
|
||||||
m.insert(-1, vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)]);
|
1,
|
||||||
m.insert(2, vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)]);
|
vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)],
|
||||||
m.insert(-2, vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)]);
|
);
|
||||||
m.insert(3, vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)]);
|
m.insert(
|
||||||
m.insert(-3, vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)]);
|
-1,
|
||||||
m.insert(4, vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)]);
|
vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)],
|
||||||
m.insert(-4, vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)]);
|
);
|
||||||
m.insert(5, vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)]);
|
m.insert(
|
||||||
m.insert(-5, vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)]);
|
2,
|
||||||
|
vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
-2,
|
||||||
|
vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
3,
|
||||||
|
vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
-3,
|
||||||
|
vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
4,
|
||||||
|
vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
-4,
|
||||||
|
vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
5,
|
||||||
|
vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)],
|
||||||
|
);
|
||||||
|
m.insert(
|
||||||
|
-5,
|
||||||
|
vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)],
|
||||||
|
);
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,11 +206,29 @@ pub const TLS_RECORD_CHANGE_CIPHER: u8 = 0x14;
|
|||||||
pub const TLS_RECORD_APPLICATION: u8 = 0x17;
|
pub const TLS_RECORD_APPLICATION: u8 = 0x17;
|
||||||
/// TLS record type: Alert
|
/// TLS record type: Alert
|
||||||
pub const TLS_RECORD_ALERT: u8 = 0x15;
|
pub const TLS_RECORD_ALERT: u8 = 0x15;
|
||||||
/// Maximum TLS record size
|
/// Maximum TLS plaintext record payload size.
|
||||||
pub const MAX_TLS_RECORD_SIZE: usize = 16384;
|
/// RFC 8446 §5.1: "The length MUST NOT exceed 2^14 bytes."
|
||||||
/// Maximum TLS chunk size (with overhead)
|
/// Use this for validating incoming unencrypted records
|
||||||
/// RFC 8446 §5.2 allows up to 16384 + 256 bytes of ciphertext
|
/// (ClientHello, ChangeCipherSpec, unprotected Handshake messages).
|
||||||
pub const MAX_TLS_CHUNK_SIZE: usize = 16384 + 256;
|
pub const MAX_TLS_PLAINTEXT_SIZE: usize = 16_384;
|
||||||
|
|
||||||
|
/// Structural minimum for a valid TLS 1.3 ClientHello with SNI.
|
||||||
|
/// Derived from RFC 8446 §4.1.2 field layout + Appendix D.4 compat mode.
|
||||||
|
/// Deliberately conservative (below any real client) to avoid false
|
||||||
|
/// positives on legitimate connections with compact extension sets.
|
||||||
|
pub const MIN_TLS_CLIENT_HELLO_SIZE: usize = 100;
|
||||||
|
|
||||||
|
/// Maximum TLS ciphertext record payload size.
|
||||||
|
/// RFC 8446 §5.2: "The length MUST NOT exceed 2^14 + 256 bytes."
|
||||||
|
/// The +256 accounts for maximum AEAD expansion overhead.
|
||||||
|
/// Use this for validating or sizing buffers for encrypted records.
|
||||||
|
pub const MAX_TLS_CIPHERTEXT_SIZE: usize = 16_384 + 256;
|
||||||
|
|
||||||
|
#[deprecated(note = "use MAX_TLS_PLAINTEXT_SIZE")]
|
||||||
|
pub const MAX_TLS_RECORD_SIZE: usize = MAX_TLS_PLAINTEXT_SIZE;
|
||||||
|
|
||||||
|
#[deprecated(note = "use MAX_TLS_CIPHERTEXT_SIZE")]
|
||||||
|
pub const MAX_TLS_CHUNK_SIZE: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||||
|
|
||||||
/// Secure Intermediate payload is expected to be 4-byte aligned.
|
/// Secure Intermediate payload is expected to be 4-byte aligned.
|
||||||
pub fn is_valid_secure_payload_len(data_len: usize) -> bool {
|
pub fn is_valid_secure_payload_len(data_len: usize) -> bool {
|
||||||
@@ -204,9 +276,7 @@ pub const SMALL_BUFFER_SIZE: usize = 8192;
|
|||||||
// ============= Statistics =============
|
// ============= Statistics =============
|
||||||
|
|
||||||
/// Duration buckets for histogram metrics
|
/// Duration buckets for histogram metrics
|
||||||
pub static DURATION_BUCKETS: &[f64] = &[
|
pub static DURATION_BUCKETS: &[f64] = &[0.1, 0.5, 1.0, 2.0, 5.0, 15.0, 60.0, 300.0, 600.0, 1800.0];
|
||||||
0.1, 0.5, 1.0, 2.0, 5.0, 15.0, 60.0, 300.0, 600.0, 1800.0,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============= Reserved Nonce Patterns =============
|
// ============= Reserved Nonce Patterns =============
|
||||||
|
|
||||||
@@ -224,22 +294,20 @@ pub static RESERVED_NONCE_BEGINNINGS: &[[u8; 4]] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// Reserved continuation bytes (bytes 4-7)
|
/// Reserved continuation bytes (bytes 4-7)
|
||||||
pub static RESERVED_NONCE_CONTINUES: &[[u8; 4]] = &[
|
pub static RESERVED_NONCE_CONTINUES: &[[u8; 4]] = &[[0x00, 0x00, 0x00, 0x00]];
|
||||||
[0x00, 0x00, 0x00, 0x00],
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============= RPC Constants (for Middle Proxy) =============
|
// ============= RPC Constants (for Middle Proxy) =============
|
||||||
|
|
||||||
/// RPC Proxy Request
|
/// RPC Proxy Request
|
||||||
/// RPC Flags (from Erlang mtp_rpc.erl)
|
/// RPC Flags (from Erlang mtp_rpc.erl)
|
||||||
pub const RPC_FLAG_NOT_ENCRYPTED: u32 = 0x2;
|
pub const RPC_FLAG_NOT_ENCRYPTED: u32 = 0x2;
|
||||||
pub const RPC_FLAG_HAS_AD_TAG: u32 = 0x8;
|
pub const RPC_FLAG_HAS_AD_TAG: u32 = 0x8;
|
||||||
pub const RPC_FLAG_MAGIC: u32 = 0x1000;
|
pub const RPC_FLAG_MAGIC: u32 = 0x1000;
|
||||||
pub const RPC_FLAG_EXTMODE2: u32 = 0x20000;
|
pub const RPC_FLAG_EXTMODE2: u32 = 0x20000;
|
||||||
pub const RPC_FLAG_PAD: u32 = 0x8000000;
|
pub const RPC_FLAG_PAD: u32 = 0x8000000;
|
||||||
pub const RPC_FLAG_INTERMEDIATE: u32 = 0x20000000;
|
pub const RPC_FLAG_INTERMEDIATE: u32 = 0x20000000;
|
||||||
pub const RPC_FLAG_ABRIDGED: u32 = 0x40000000;
|
pub const RPC_FLAG_ABRIDGED: u32 = 0x40000000;
|
||||||
pub const RPC_FLAG_QUICKACK: u32 = 0x80000000;
|
pub const RPC_FLAG_QUICKACK: u32 = 0x80000000;
|
||||||
|
|
||||||
pub const RPC_PROXY_REQ: [u8; 4] = [0xee, 0xf1, 0xce, 0x36];
|
pub const RPC_PROXY_REQ: [u8; 4] = [0xee, 0xf1, 0xce, 0x36];
|
||||||
/// RPC Proxy Answer
|
/// RPC Proxy Answer
|
||||||
@@ -267,60 +335,63 @@ pub mod rpc_flags {
|
|||||||
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============= Middle-End Proxy Servers =============
|
||||||
|
pub const ME_PROXY_PORT: u16 = 8888;
|
||||||
|
|
||||||
// ============= Middle-End Proxy Servers =============
|
pub static TG_MIDDLE_PROXIES_FLAT_V4: LazyLock<Vec<(IpAddr, u16)>> = LazyLock::new(|| {
|
||||||
pub const ME_PROXY_PORT: u16 = 8888;
|
vec![
|
||||||
|
(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888),
|
||||||
|
(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888),
|
||||||
|
(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888),
|
||||||
|
(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888),
|
||||||
|
(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
pub static TG_MIDDLE_PROXIES_FLAT_V4: LazyLock<Vec<(IpAddr, u16)>> = LazyLock::new(|| {
|
// ============= RPC Constants (u32 native endian) =============
|
||||||
vec![
|
// From mtproto-common.h + net-tcp-rpc-common.h + mtproto-proxy.c
|
||||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888),
|
|
||||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888),
|
|
||||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888),
|
|
||||||
(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888),
|
|
||||||
(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888),
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============= RPC Constants (u32 native endian) =============
|
pub const RPC_NONCE_U32: u32 = 0x7acb87aa;
|
||||||
// From mtproto-common.h + net-tcp-rpc-common.h + mtproto-proxy.c
|
pub const RPC_HANDSHAKE_U32: u32 = 0x7682eef5;
|
||||||
|
pub const RPC_HANDSHAKE_ERROR_U32: u32 = 0x6a27beda;
|
||||||
|
pub const TL_PROXY_TAG_U32: u32 = 0xdb1e26ae; // mtproto-proxy.c:121
|
||||||
|
|
||||||
pub const RPC_NONCE_U32: u32 = 0x7acb87aa;
|
// mtproto-common.h
|
||||||
pub const RPC_HANDSHAKE_U32: u32 = 0x7682eef5;
|
pub const RPC_PROXY_REQ_U32: u32 = 0x36cef1ee;
|
||||||
pub const RPC_HANDSHAKE_ERROR_U32: u32 = 0x6a27beda;
|
pub const RPC_PROXY_ANS_U32: u32 = 0x4403da0d;
|
||||||
pub const TL_PROXY_TAG_U32: u32 = 0xdb1e26ae; // mtproto-proxy.c:121
|
pub const RPC_CLOSE_CONN_U32: u32 = 0x1fcf425d;
|
||||||
|
pub const RPC_CLOSE_EXT_U32: u32 = 0x5eb634a2;
|
||||||
|
pub const RPC_SIMPLE_ACK_U32: u32 = 0x3bac409b;
|
||||||
|
pub const RPC_PING_U32: u32 = 0x5730a2df;
|
||||||
|
pub const RPC_PONG_U32: u32 = 0x8430eaa7;
|
||||||
|
|
||||||
// mtproto-common.h
|
pub const RPC_CRYPTO_NONE_U32: u32 = 0;
|
||||||
pub const RPC_PROXY_REQ_U32: u32 = 0x36cef1ee;
|
pub const RPC_CRYPTO_AES_U32: u32 = 1;
|
||||||
pub const RPC_PROXY_ANS_U32: u32 = 0x4403da0d;
|
|
||||||
pub const RPC_CLOSE_CONN_U32: u32 = 0x1fcf425d;
|
|
||||||
pub const RPC_CLOSE_EXT_U32: u32 = 0x5eb634a2;
|
|
||||||
pub const RPC_SIMPLE_ACK_U32: u32 = 0x3bac409b;
|
|
||||||
pub const RPC_PING_U32: u32 = 0x5730a2df;
|
|
||||||
pub const RPC_PONG_U32: u32 = 0x8430eaa7;
|
|
||||||
|
|
||||||
pub const RPC_CRYPTO_NONE_U32: u32 = 0;
|
pub mod proxy_flags {
|
||||||
pub const RPC_CRYPTO_AES_U32: u32 = 1;
|
pub const FLAG_HAS_AD_TAG: u32 = 1;
|
||||||
|
pub const FLAG_NOT_ENCRYPTED: u32 = 0x2;
|
||||||
|
pub const FLAG_HAS_AD_TAG2: u32 = 0x8;
|
||||||
|
pub const FLAG_MAGIC: u32 = 0x1000;
|
||||||
|
pub const FLAG_EXTMODE2: u32 = 0x20000;
|
||||||
|
pub const FLAG_PAD: u32 = 0x8000000;
|
||||||
|
pub const FLAG_INTERMEDIATE: u32 = 0x20000000;
|
||||||
|
pub const FLAG_ABRIDGED: u32 = 0x40000000;
|
||||||
|
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
||||||
|
}
|
||||||
|
|
||||||
pub mod proxy_flags {
|
pub mod rpc_crypto_flags {
|
||||||
pub const FLAG_HAS_AD_TAG: u32 = 1;
|
pub const USE_CRC32C: u32 = 0x800;
|
||||||
pub const FLAG_NOT_ENCRYPTED: u32 = 0x2;
|
}
|
||||||
pub const FLAG_HAS_AD_TAG2: u32 = 0x8;
|
|
||||||
pub const FLAG_MAGIC: u32 = 0x1000;
|
|
||||||
pub const FLAG_EXTMODE2: u32 = 0x20000;
|
|
||||||
pub const FLAG_PAD: u32 = 0x8000000;
|
|
||||||
pub const FLAG_INTERMEDIATE: u32 = 0x20000000;
|
|
||||||
pub const FLAG_ABRIDGED: u32 = 0x40000000;
|
|
||||||
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod rpc_crypto_flags {
|
pub const ME_CONNECT_TIMEOUT_SECS: u64 = 5;
|
||||||
pub const USE_CRC32C: u32 = 0x800;
|
pub const ME_HANDSHAKE_TIMEOUT_SECS: u64 = 10;
|
||||||
}
|
|
||||||
|
|
||||||
pub const ME_CONNECT_TIMEOUT_SECS: u64 = 5;
|
#[cfg(test)]
|
||||||
pub const ME_HANDSHAKE_TIMEOUT_SECS: u64 = 10;
|
#[path = "tests/tls_size_constants_security_tests.rs"]
|
||||||
|
mod tls_size_constants_security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ impl FrameMode {
|
|||||||
FrameMode::Abridged => 4,
|
FrameMode::Abridged => 4,
|
||||||
FrameMode::Intermediate => 4,
|
FrameMode::Intermediate => 4,
|
||||||
FrameMode::SecureIntermediate => 4 + 3, // length + padding
|
FrameMode::SecureIntermediate => 4 + 3, // length + padding
|
||||||
FrameMode::Full => 12 + 16, // header + max CBC padding
|
FrameMode::Full => 12 + 16, // header + max CBC padding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate message length for MTProto
|
/// Validate message length for MTProto
|
||||||
pub fn validate_message_length(len: usize) -> bool {
|
pub fn validate_message_length(len: usize) -> bool {
|
||||||
use super::constants::{MIN_MSG_LEN, MAX_MSG_LEN, PADDING_FILLER};
|
use super::constants::{MAX_MSG_LEN, MIN_MSG_LEN, PADDING_FILLER};
|
||||||
|
|
||||||
(MIN_MSG_LEN..=MAX_MSG_LEN).contains(&len) && len.is_multiple_of(PADDING_FILLER.len())
|
(MIN_MSG_LEN..=MAX_MSG_LEN).contains(&len) && len.is_multiple_of(PADDING_FILLER.len())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
use crate::crypto::{sha256, AesCtr};
|
|
||||||
use super::constants::*;
|
use super::constants::*;
|
||||||
|
use crate::crypto::{AesCtr, sha256};
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
/// Obfuscation parameters from handshake
|
/// Obfuscation parameters from handshake
|
||||||
///
|
///
|
||||||
@@ -69,9 +69,8 @@ impl ObfuscationParams {
|
|||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let dc_idx = i16::from_le_bytes(
|
let dc_idx =
|
||||||
decrypted[DC_IDX_POS..DC_IDX_POS + 2].try_into().unwrap()
|
i16::from_le_bytes(decrypted[DC_IDX_POS..DC_IDX_POS + 2].try_into().unwrap());
|
||||||
);
|
|
||||||
|
|
||||||
let mut enc_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
|
let mut enc_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
|
||||||
enc_key_input.extend_from_slice(enc_prekey);
|
enc_key_input.extend_from_slice(enc_prekey);
|
||||||
|
|||||||
+15
-9
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::time::Instant;
|
|
||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
/// Helper to create a byte vector of specific length.
|
/// Helper to create a byte vector of specific length.
|
||||||
fn make_garbage(len: usize) -> Vec<u8> {
|
fn make_garbage(len: usize) -> Vec<u8> {
|
||||||
@@ -33,8 +33,7 @@ fn make_valid_tls_handshake_with_session_id(
|
|||||||
|
|
||||||
let digest = make_digest(secret, &handshake, timestamp);
|
let digest = make_digest(secret, &handshake, timestamp);
|
||||||
|
|
||||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&digest);
|
||||||
.copy_from_slice(&digest);
|
|
||||||
handshake
|
handshake
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +160,10 @@ fn extract_sni_with_invalid_hostname_rejected() {
|
|||||||
h.extend_from_slice(&(ext.len() as u16).to_be_bytes());
|
h.extend_from_slice(&(ext.len() as u16).to_be_bytes());
|
||||||
h.extend_from_slice(&ext);
|
h.extend_from_slice(&ext);
|
||||||
|
|
||||||
assert!(extract_sni_from_client_hello(&h).is_none(), "Invalid SNI hostname must be rejected");
|
assert!(
|
||||||
|
extract_sni_from_client_hello(&h).is_none(),
|
||||||
|
"Invalid SNI hostname must be rejected"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -307,9 +309,8 @@ fn extract_sni_with_duplicate_extensions_rejected() {
|
|||||||
h.extend_from_slice(&(handshake.len() as u16).to_be_bytes());
|
h.extend_from_slice(&(handshake.len() as u16).to_be_bytes());
|
||||||
h.extend_from_slice(&handshake);
|
h.extend_from_slice(&handshake);
|
||||||
|
|
||||||
// Parser might return first, see second, or fail. OWASP ASVS prefers rejection of unexpected dups.
|
// Duplicate SNI extensions are ambiguous and must fail closed.
|
||||||
// Telemt's `extract_sni` returns the first one found.
|
assert!(extract_sni_from_client_hello(&h).is_none());
|
||||||
assert!(extract_sni_from_client_hello(&h).is_some());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -324,7 +325,9 @@ fn extract_alpn_with_malformed_list_rejected() {
|
|||||||
ext.extend_from_slice(&(alpn_payload.len() as u16).to_be_bytes());
|
ext.extend_from_slice(&(alpn_payload.len() as u16).to_be_bytes());
|
||||||
ext.extend_from_slice(&alpn_payload);
|
ext.extend_from_slice(&alpn_payload);
|
||||||
|
|
||||||
let mut h = vec![0x16, 0x03, 0x03, 0x00, 0x40, 0x01, 0x00, 0x00, 0x3C, 0x03, 0x03];
|
let mut h = vec![
|
||||||
|
0x16, 0x03, 0x03, 0x00, 0x40, 0x01, 0x00, 0x00, 0x3C, 0x03, 0x03,
|
||||||
|
];
|
||||||
h.extend_from_slice(&[0u8; 32]);
|
h.extend_from_slice(&[0u8; 32]);
|
||||||
h.push(0);
|
h.push(0);
|
||||||
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01, 0x01, 0x00]);
|
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01, 0x01, 0x00]);
|
||||||
@@ -332,7 +335,10 @@ fn extract_alpn_with_malformed_list_rejected() {
|
|||||||
h.extend_from_slice(&ext);
|
h.extend_from_slice(&ext);
|
||||||
|
|
||||||
let res = extract_alpn_from_client_hello(&h);
|
let res = extract_alpn_from_client_hello(&h);
|
||||||
assert!(res.is_empty(), "Malformed ALPN list must return empty or fail");
|
assert!(
|
||||||
|
res.is_empty(),
|
||||||
|
"Malformed ALPN list must return empty or fail"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
+21
-6
@@ -84,7 +84,10 @@ fn make_valid_client_hello_record(host: &str, alpn_protocols: &[&[u8]]) -> Vec<u
|
|||||||
#[test]
|
#[test]
|
||||||
fn client_hello_fuzz_corpus_never_panics_or_accepts_corruption() {
|
fn client_hello_fuzz_corpus_never_panics_or_accepts_corruption() {
|
||||||
let valid = make_valid_client_hello_record("example.com", &[b"h2", b"http/1.1"]);
|
let valid = make_valid_client_hello_record("example.com", &[b"h2", b"http/1.1"]);
|
||||||
assert_eq!(extract_sni_from_client_hello(&valid).as_deref(), Some("example.com"));
|
assert_eq!(
|
||||||
|
extract_sni_from_client_hello(&valid).as_deref(),
|
||||||
|
Some("example.com")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extract_alpn_from_client_hello(&valid),
|
extract_alpn_from_client_hello(&valid),
|
||||||
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
||||||
@@ -121,8 +124,14 @@ fn client_hello_fuzz_corpus_never_panics_or_accepts_corruption() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(extract_sni_from_client_hello(input).is_none(), "corpus item {idx} must fail closed for SNI");
|
assert!(
|
||||||
assert!(extract_alpn_from_client_hello(input).is_empty(), "corpus item {idx} must fail closed for ALPN");
|
extract_sni_from_client_hello(input).is_none(),
|
||||||
|
"corpus item {idx} must fail closed for SNI"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
extract_alpn_from_client_hello(input).is_empty(),
|
||||||
|
"corpus item {idx} must fail closed for ALPN"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +172,9 @@ fn tls_handshake_fuzz_corpus_never_panics_and_rejects_digest_mutations() {
|
|||||||
for _ in 0..32 {
|
for _ in 0..32 {
|
||||||
let mut mutated = base.clone();
|
let mut mutated = base.clone();
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
seed = seed.wrapping_mul(2862933555777941757).wrapping_add(3037000493);
|
seed = seed
|
||||||
|
.wrapping_mul(2862933555777941757)
|
||||||
|
.wrapping_add(3037000493);
|
||||||
let idx = TLS_DIGEST_POS + (seed as usize % TLS_DIGEST_LEN);
|
let idx = TLS_DIGEST_POS + (seed as usize % TLS_DIGEST_LEN);
|
||||||
mutated[idx] ^= ((seed >> 17) as u8).wrapping_add(1);
|
mutated[idx] ^= ((seed >> 17) as u8).wrapping_add(1);
|
||||||
}
|
}
|
||||||
@@ -171,9 +182,13 @@ fn tls_handshake_fuzz_corpus_never_panics_and_rejects_digest_mutations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (idx, handshake) in corpus.iter().enumerate() {
|
for (idx, handshake) in corpus.iter().enumerate() {
|
||||||
let result = catch_unwind(|| validate_tls_handshake_at_time(handshake, &secrets, false, now));
|
let result =
|
||||||
|
catch_unwind(|| validate_tls_handshake_at_time(handshake, &secrets, false, now));
|
||||||
assert!(result.is_ok(), "corpus item {idx} must not panic");
|
assert!(result.is_ok(), "corpus item {idx} must not panic");
|
||||||
assert!(result.unwrap().is_none(), "corpus item {idx} must fail closed");
|
assert!(
|
||||||
|
result.unwrap().is_none(),
|
||||||
|
"corpus item {idx} must fail closed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extension_builder_fails_closed_on_u16_length_overflow() {
|
||||||
|
let builder = TlsExtensionBuilder {
|
||||||
|
extensions: vec![0u8; (u16::MAX as usize) + 1],
|
||||||
|
};
|
||||||
|
|
||||||
|
let built = builder.build();
|
||||||
|
assert!(
|
||||||
|
built.is_empty(),
|
||||||
|
"oversized extension blob must fail closed instead of truncating length field"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn server_hello_builder_fails_closed_on_session_id_len_overflow() {
|
||||||
|
let builder = ServerHelloBuilder {
|
||||||
|
random: [0u8; 32],
|
||||||
|
session_id: vec![0xAB; (u8::MAX as usize) + 1],
|
||||||
|
cipher_suite: cipher_suite::TLS_AES_128_GCM_SHA256,
|
||||||
|
compression: 0,
|
||||||
|
extensions: TlsExtensionBuilder::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = builder.build_message();
|
||||||
|
let record = builder.build_record();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
message.is_empty(),
|
||||||
|
"session_id length overflow must fail closed in message builder"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
record.is_empty(),
|
||||||
|
"session_id length overflow must fail closed in record builder"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
use crate::tls_front::emulator::build_emulated_server_hello;
|
use crate::tls_front::emulator::build_emulated_server_hello;
|
||||||
use crate::tls_front::types::{CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsProfileSource};
|
use crate::tls_front::types::{
|
||||||
|
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsProfileSource,
|
||||||
|
};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
/// Build a TLS-handshake-like buffer that contains a valid HMAC digest
|
/// Build a TLS-handshake-like buffer that contains a valid HMAC digest
|
||||||
@@ -39,8 +41,7 @@ fn make_valid_tls_handshake_with_session_id(
|
|||||||
digest[28 + i] ^= ts[i];
|
digest[28 + i] ^= ts[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&digest);
|
||||||
.copy_from_slice(&digest);
|
|
||||||
handshake
|
handshake
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +181,10 @@ fn second_user_in_list_found_when_first_does_not_match() {
|
|||||||
("user_b".to_string(), secret_b.to_vec()),
|
("user_b".to_string(), secret_b.to_vec()),
|
||||||
];
|
];
|
||||||
let result = validate_tls_handshake(&handshake, &secrets, true);
|
let result = validate_tls_handshake(&handshake, &secrets, true);
|
||||||
assert!(result.is_some(), "user_b must be found even though user_a comes first");
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"user_b must be found even though user_a comes first"
|
||||||
|
);
|
||||||
assert_eq!(result.unwrap().user, "user_b");
|
assert_eq!(result.unwrap().user, "user_b");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,8 +432,7 @@ fn censor_probe_random_digests_all_rejected() {
|
|||||||
let mut h = vec![0x42u8; min_len];
|
let mut h = vec![0x42u8; min_len];
|
||||||
h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = session_id_len as u8;
|
h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = session_id_len as u8;
|
||||||
let rand_digest = rng.bytes(TLS_DIGEST_LEN);
|
let rand_digest = rng.bytes(TLS_DIGEST_LEN);
|
||||||
h[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
h[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&rand_digest);
|
||||||
.copy_from_slice(&rand_digest);
|
|
||||||
assert!(
|
assert!(
|
||||||
validate_tls_handshake(&h, &secrets, true).is_none(),
|
validate_tls_handshake(&h, &secrets, true).is_none(),
|
||||||
"Random digest at attempt {attempt} must not match"
|
"Random digest at attempt {attempt} must not match"
|
||||||
@@ -553,8 +556,7 @@ fn system_time_before_unix_epoch_is_rejected_without_panic() {
|
|||||||
fn system_time_far_future_overflowing_i64_returns_none() {
|
fn system_time_far_future_overflowing_i64_returns_none() {
|
||||||
// i64::MAX + 1 seconds past epoch overflows i64 when cast naively with `as`.
|
// i64::MAX + 1 seconds past epoch overflows i64 when cast naively with `as`.
|
||||||
let overflow_secs = u64::try_from(i64::MAX).unwrap() + 1;
|
let overflow_secs = u64::try_from(i64::MAX).unwrap() + 1;
|
||||||
if let Some(far_future) =
|
if let Some(far_future) = UNIX_EPOCH.checked_add(std::time::Duration::from_secs(overflow_secs))
|
||||||
UNIX_EPOCH.checked_add(std::time::Duration::from_secs(overflow_secs))
|
|
||||||
{
|
{
|
||||||
assert!(
|
assert!(
|
||||||
system_time_to_unix_secs(far_future).is_none(),
|
system_time_to_unix_secs(far_future).is_none(),
|
||||||
@@ -620,7 +622,10 @@ fn appended_trailing_byte_causes_rejection() {
|
|||||||
let mut h = make_valid_tls_handshake(secret, 0);
|
let mut h = make_valid_tls_handshake(secret, 0);
|
||||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||||
|
|
||||||
assert!(validate_tls_handshake(&h, &secrets, true).is_some(), "baseline");
|
assert!(
|
||||||
|
validate_tls_handshake(&h, &secrets, true).is_some(),
|
||||||
|
"baseline"
|
||||||
|
);
|
||||||
|
|
||||||
h.push(0x00);
|
h.push(0x00);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -647,8 +652,7 @@ fn zero_length_session_id_accepted() {
|
|||||||
|
|
||||||
let computed = sha256_hmac(secret, &handshake);
|
let computed = sha256_hmac(secret, &handshake);
|
||||||
// timestamp = 0 → ts XOR bytes are all zero → digest = computed unchanged.
|
// timestamp = 0 → ts XOR bytes are all zero → digest = computed unchanged.
|
||||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&computed);
|
||||||
.copy_from_slice(&computed);
|
|
||||||
|
|
||||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||||
let result = validate_tls_handshake(&handshake, &secrets, true);
|
let result = validate_tls_handshake(&handshake, &secrets, true);
|
||||||
@@ -773,10 +777,18 @@ fn ignore_time_skew_explicitly_decouples_from_boot_time_cap() {
|
|||||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||||
|
|
||||||
let cap_zero = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, 0);
|
let cap_zero = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, 0);
|
||||||
let cap_nonzero =
|
let cap_nonzero = validate_tls_handshake_at_time_with_boot_cap(
|
||||||
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, BOOT_TIME_COMPAT_MAX_SECS);
|
&h,
|
||||||
|
&secrets,
|
||||||
|
true,
|
||||||
|
0,
|
||||||
|
BOOT_TIME_COMPAT_MAX_SECS,
|
||||||
|
);
|
||||||
|
|
||||||
assert!(cap_zero.is_some(), "ignore_time_skew=true must accept valid HMAC");
|
assert!(
|
||||||
|
cap_zero.is_some(),
|
||||||
|
"ignore_time_skew=true must accept valid HMAC"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
cap_nonzero.is_some(),
|
cap_nonzero.is_some(),
|
||||||
"ignore_time_skew path must not depend on boot-time cap"
|
"ignore_time_skew path must not depend on boot-time cap"
|
||||||
@@ -888,8 +900,8 @@ fn adversarial_skew_boundary_matrix_accepts_only_inclusive_window_when_boot_disa
|
|||||||
let ts_i64 = now - offset;
|
let ts_i64 = now - offset;
|
||||||
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for test matrix");
|
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for test matrix");
|
||||||
let h = make_valid_tls_handshake(secret, ts);
|
let h = make_valid_tls_handshake(secret, ts);
|
||||||
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
|
let accepted =
|
||||||
.is_some();
|
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0).is_some();
|
||||||
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&offset);
|
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&offset);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
accepted, expected,
|
accepted, expected,
|
||||||
@@ -917,8 +929,8 @@ fn light_fuzz_skew_window_rejects_outside_range_when_boot_disabled() {
|
|||||||
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for fuzz test");
|
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for fuzz test");
|
||||||
|
|
||||||
let h = make_valid_tls_handshake(secret, ts);
|
let h = make_valid_tls_handshake(secret, ts);
|
||||||
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
|
let accepted =
|
||||||
.is_some();
|
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0).is_some();
|
||||||
assert!(
|
assert!(
|
||||||
!accepted,
|
!accepted,
|
||||||
"offset {offset} must be rejected outside strict skew window"
|
"offset {offset} must be rejected outside strict skew window"
|
||||||
@@ -940,8 +952,8 @@ fn stress_boot_disabled_validation_matches_time_diff_oracle() {
|
|||||||
let ts = s as u32;
|
let ts = s as u32;
|
||||||
let h = make_valid_tls_handshake(secret, ts);
|
let h = make_valid_tls_handshake(secret, ts);
|
||||||
|
|
||||||
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
|
let accepted =
|
||||||
.is_some();
|
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0).is_some();
|
||||||
let time_diff = now - i64::from(ts);
|
let time_diff = now - i64::from(ts);
|
||||||
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&time_diff);
|
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&time_diff);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -960,7 +972,10 @@ fn integration_large_user_list_with_boot_disabled_finds_only_matching_user() {
|
|||||||
|
|
||||||
let mut secrets = Vec::new();
|
let mut secrets = Vec::new();
|
||||||
for i in 0..512u32 {
|
for i in 0..512u32 {
|
||||||
secrets.push((format!("noise-{i}"), format!("noise-secret-{i}").into_bytes()));
|
secrets.push((
|
||||||
|
format!("noise-{i}"),
|
||||||
|
format!("noise-secret-{i}").into_bytes(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
secrets.push(("target-user".to_string(), target_secret.to_vec()));
|
secrets.push(("target-user".to_string(), target_secret.to_vec()));
|
||||||
|
|
||||||
@@ -1018,7 +1033,10 @@ fn u32_max_timestamp_accepted_with_ignore_time_skew() {
|
|||||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||||
|
|
||||||
let result = validate_tls_handshake(&h, &secrets, true);
|
let result = validate_tls_handshake(&h, &secrets, true);
|
||||||
assert!(result.is_some(), "u32::MAX timestamp must be accepted with ignore_time_skew=true");
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"u32::MAX timestamp must be accepted with ignore_time_skew=true"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap().timestamp,
|
result.unwrap().timestamp,
|
||||||
u32::MAX,
|
u32::MAX,
|
||||||
@@ -1150,16 +1168,17 @@ fn first_matching_user_wins_over_later_duplicate_secret() {
|
|||||||
|
|
||||||
let secrets = vec![
|
let secrets = vec![
|
||||||
("decoy_1".to_string(), b"wrong_1".to_vec()),
|
("decoy_1".to_string(), b"wrong_1".to_vec()),
|
||||||
("winner".to_string(), shared.to_vec()), // first match
|
("winner".to_string(), shared.to_vec()), // first match
|
||||||
("decoy_2".to_string(), b"wrong_2".to_vec()),
|
("decoy_2".to_string(), b"wrong_2".to_vec()),
|
||||||
("loser".to_string(), shared.to_vec()), // second match — must not win
|
("loser".to_string(), shared.to_vec()), // second match — must not win
|
||||||
("decoy_3".to_string(), b"wrong_3".to_vec()),
|
("decoy_3".to_string(), b"wrong_3".to_vec()),
|
||||||
];
|
];
|
||||||
|
|
||||||
let result = validate_tls_handshake(&h, &secrets, true);
|
let result = validate_tls_handshake(&h, &secrets, true);
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap().user, "winner",
|
result.unwrap().user,
|
||||||
|
"winner",
|
||||||
"first matching user must be returned even when a later entry also matches"
|
"first matching user must be returned even when a later entry also matches"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1189,7 +1208,7 @@ fn test_parse_tls_record_header() {
|
|||||||
let header = [0x17, 0x03, 0x03, 0x40, 0x00];
|
let header = [0x17, 0x03, 0x03, 0x40, 0x00];
|
||||||
let result = parse_tls_record_header(&header).unwrap();
|
let result = parse_tls_record_header(&header).unwrap();
|
||||||
assert_eq!(result.0, TLS_RECORD_APPLICATION);
|
assert_eq!(result.0, TLS_RECORD_APPLICATION);
|
||||||
assert_eq!(result.1, 16384);
|
assert_eq!(usize::from(result.1), MAX_TLS_PLAINTEXT_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1425,7 +1444,8 @@ fn test_build_server_hello_structure() {
|
|||||||
assert!(response.len() > ccs_start + 6);
|
assert!(response.len() > ccs_start + 6);
|
||||||
assert_eq!(response[ccs_start], TLS_RECORD_CHANGE_CIPHER);
|
assert_eq!(response[ccs_start], TLS_RECORD_CHANGE_CIPHER);
|
||||||
|
|
||||||
let ccs_len = 5 + u16::from_be_bytes([response[ccs_start + 3], response[ccs_start + 4]]) as usize;
|
let ccs_len =
|
||||||
|
5 + u16::from_be_bytes([response[ccs_start + 3], response[ccs_start + 4]]) as usize;
|
||||||
let app_start = ccs_start + ccs_len;
|
let app_start = ccs_start + ccs_len;
|
||||||
assert!(response.len() > app_start + 5);
|
assert!(response.len() > app_start + 5);
|
||||||
assert_eq!(response[app_start], TLS_RECORD_APPLICATION);
|
assert_eq!(response[app_start], TLS_RECORD_APPLICATION);
|
||||||
@@ -1729,7 +1749,10 @@ fn empty_secret_hmac_is_supported() {
|
|||||||
let handshake = make_valid_tls_handshake(secret, 0);
|
let handshake = make_valid_tls_handshake(secret, 0);
|
||||||
let secrets = vec![("empty".to_string(), secret.to_vec())];
|
let secrets = vec![("empty".to_string(), secret.to_vec())];
|
||||||
let result = validate_tls_handshake(&handshake, &secrets, true);
|
let result = validate_tls_handshake(&handshake, &secrets, true);
|
||||||
assert!(result.is_some(), "Empty HMAC key must not panic and must validate when correct");
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"Empty HMAC key must not panic and must validate when correct"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1802,7 +1825,10 @@ fn server_hello_application_data_payload_varies_across_runs() {
|
|||||||
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
||||||
let payload = response[app_pos + 5..app_pos + 5 + app_len].to_vec();
|
let payload = response[app_pos + 5..app_pos + 5 + app_len].to_vec();
|
||||||
|
|
||||||
assert!(payload.iter().any(|&b| b != 0), "Payload must not be all-zero deterministic filler");
|
assert!(
|
||||||
|
payload.iter().any(|&b| b != 0),
|
||||||
|
"Payload must not be all-zero deterministic filler"
|
||||||
|
);
|
||||||
unique_payloads.insert(payload);
|
unique_payloads.insert(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1846,7 +1872,13 @@ fn large_replay_window_does_not_expand_time_skew_acceptance() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_tls_record_header_accepts_tls_version_constant() {
|
fn parse_tls_record_header_accepts_tls_version_constant() {
|
||||||
let header = [TLS_RECORD_HANDSHAKE, TLS_VERSION[0], TLS_VERSION[1], 0x00, 0x2A];
|
let header = [
|
||||||
|
TLS_RECORD_HANDSHAKE,
|
||||||
|
TLS_VERSION[0],
|
||||||
|
TLS_VERSION[1],
|
||||||
|
0x00,
|
||||||
|
0x2A,
|
||||||
|
];
|
||||||
let parsed = parse_tls_record_header(&header).expect("TLS_VERSION header should be accepted");
|
let parsed = parse_tls_record_header(&header).expect("TLS_VERSION header should be accepted");
|
||||||
assert_eq!(parsed.0, TLS_RECORD_HANDSHAKE);
|
assert_eq!(parsed.0, TLS_RECORD_HANDSHAKE);
|
||||||
assert_eq!(parsed.1, 42);
|
assert_eq!(parsed.1, 42);
|
||||||
@@ -1868,7 +1900,10 @@ fn server_hello_clamps_fake_cert_len_lower_bound() {
|
|||||||
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
||||||
|
|
||||||
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
||||||
assert_eq!(app_len, 64, "fake cert payload must be clamped to minimum 64 bytes");
|
assert_eq!(
|
||||||
|
app_len, 64,
|
||||||
|
"fake cert payload must be clamped to minimum 64 bytes"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1887,7 +1922,10 @@ fn server_hello_clamps_fake_cert_len_upper_bound() {
|
|||||||
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
||||||
|
|
||||||
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
||||||
assert_eq!(app_len, 16_640, "fake cert payload must be clamped to TLS record max bound");
|
assert_eq!(
|
||||||
|
app_len, MAX_TLS_CIPHERTEXT_SIZE,
|
||||||
|
"fake cert payload must be clamped to TLS record max bound"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1898,7 +1936,15 @@ fn server_hello_new_session_ticket_count_matches_configuration() {
|
|||||||
let rng = crate::crypto::SecureRandom::new();
|
let rng = crate::crypto::SecureRandom::new();
|
||||||
|
|
||||||
let tickets: u8 = 3;
|
let tickets: u8 = 3;
|
||||||
let response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, tickets);
|
let response = build_server_hello(
|
||||||
|
secret,
|
||||||
|
&client_digest,
|
||||||
|
&session_id,
|
||||||
|
1024,
|
||||||
|
&rng,
|
||||||
|
None,
|
||||||
|
tickets,
|
||||||
|
);
|
||||||
|
|
||||||
let mut pos = 0usize;
|
let mut pos = 0usize;
|
||||||
let mut app_records = 0usize;
|
let mut app_records = 0usize;
|
||||||
@@ -1906,7 +1952,10 @@ fn server_hello_new_session_ticket_count_matches_configuration() {
|
|||||||
let rtype = response[pos];
|
let rtype = response[pos];
|
||||||
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
||||||
let next = pos + 5 + rlen;
|
let next = pos + 5 + rlen;
|
||||||
assert!(next <= response.len(), "TLS record must stay inside response bounds");
|
assert!(
|
||||||
|
next <= response.len(),
|
||||||
|
"TLS record must stay inside response bounds"
|
||||||
|
);
|
||||||
if rtype == TLS_RECORD_APPLICATION {
|
if rtype == TLS_RECORD_APPLICATION {
|
||||||
app_records += 1;
|
app_records += 1;
|
||||||
}
|
}
|
||||||
@@ -1927,7 +1976,15 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
|||||||
let session_id = vec![0x54; 32];
|
let session_id = vec![0x54; 32];
|
||||||
let rng = crate::crypto::SecureRandom::new();
|
let rng = crate::crypto::SecureRandom::new();
|
||||||
|
|
||||||
let response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, u8::MAX);
|
let response = build_server_hello(
|
||||||
|
secret,
|
||||||
|
&client_digest,
|
||||||
|
&session_id,
|
||||||
|
1024,
|
||||||
|
&rng,
|
||||||
|
None,
|
||||||
|
u8::MAX,
|
||||||
|
);
|
||||||
|
|
||||||
let mut pos = 0usize;
|
let mut pos = 0usize;
|
||||||
let mut app_records = 0usize;
|
let mut app_records = 0usize;
|
||||||
@@ -1935,7 +1992,10 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
|||||||
let rtype = response[pos];
|
let rtype = response[pos];
|
||||||
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
||||||
let next = pos + 5 + rlen;
|
let next = pos + 5 + rlen;
|
||||||
assert!(next <= response.len(), "TLS record must stay inside response bounds");
|
assert!(
|
||||||
|
next <= response.len(),
|
||||||
|
"TLS record must stay inside response bounds"
|
||||||
|
);
|
||||||
if rtype == TLS_RECORD_APPLICATION {
|
if rtype == TLS_RECORD_APPLICATION {
|
||||||
app_records += 1;
|
app_records += 1;
|
||||||
}
|
}
|
||||||
@@ -1943,8 +2003,7 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app_records,
|
app_records, 5,
|
||||||
5,
|
|
||||||
"response must cap ticket-like tail records to four plus one main application record"
|
"response must cap ticket-like tail records to four plus one main application record"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1972,10 +2031,14 @@ fn boot_time_handshake_replay_remains_blocked_after_cache_window_expires() {
|
|||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(70));
|
std::thread::sleep(std::time::Duration::from_millis(70));
|
||||||
|
|
||||||
let validation_after_expiry = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
let validation_after_expiry =
|
||||||
.expect("boot-time handshake must still cryptographically validate after cache expiry");
|
validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
||||||
|
.expect("boot-time handshake must still cryptographically validate after cache expiry");
|
||||||
let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN];
|
let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN];
|
||||||
assert_eq!(digest_half, digest_half_after_expiry, "replay key must be stable for same handshake");
|
assert_eq!(
|
||||||
|
digest_half, digest_half_after_expiry,
|
||||||
|
"replay key must be stable for same handshake"
|
||||||
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
checker.check_and_add_tls_digest(digest_half_after_expiry),
|
checker.check_and_add_tls_digest(digest_half_after_expiry),
|
||||||
@@ -2006,8 +2069,9 @@ fn adversarial_boot_time_handshake_should_not_be_replayable_after_cache_expiry()
|
|||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(70));
|
std::thread::sleep(std::time::Duration::from_millis(70));
|
||||||
|
|
||||||
let validation_after_expiry = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
let validation_after_expiry =
|
||||||
.expect("boot-time handshake still validates cryptographically after cache expiry");
|
validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
||||||
|
.expect("boot-time handshake still validates cryptographically after cache expiry");
|
||||||
let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN];
|
let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN];
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2067,11 +2131,14 @@ fn light_fuzz_boot_time_timestamp_matrix_with_short_replay_window_obeys_boot_cap
|
|||||||
let ts = (s as u32) % 8;
|
let ts = (s as u32) % 8;
|
||||||
|
|
||||||
let handshake = make_valid_tls_handshake(secret, ts);
|
let handshake = make_valid_tls_handshake(secret, ts);
|
||||||
let accepted = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
let accepted =
|
||||||
.is_some();
|
validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2).is_some();
|
||||||
|
|
||||||
if ts < 2 {
|
if ts < 2 {
|
||||||
assert!(accepted, "timestamp {ts} must remain boot-time compatible under 2s cap");
|
assert!(
|
||||||
|
accepted,
|
||||||
|
"timestamp {ts} must remain boot-time compatible under 2s cap"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
assert!(
|
assert!(
|
||||||
!accepted,
|
!accepted,
|
||||||
@@ -2107,7 +2174,9 @@ fn server_hello_application_data_contains_alpn_marker_when_selected() {
|
|||||||
|
|
||||||
let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
|
let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
|
||||||
assert!(
|
assert!(
|
||||||
app_payload.windows(expected.len()).any(|window| window == expected),
|
app_payload
|
||||||
|
.windows(expected.len())
|
||||||
|
.any(|window| window == expected),
|
||||||
"first application payload must carry ALPN marker for selected protocol"
|
"first application payload must carry ALPN marker for selected protocol"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2137,7 +2206,10 @@ fn server_hello_ignores_oversized_alpn_and_still_caps_ticket_tail() {
|
|||||||
let rtype = response[pos];
|
let rtype = response[pos];
|
||||||
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
||||||
let next = pos + 5 + rlen;
|
let next = pos + 5 + rlen;
|
||||||
assert!(next <= response.len(), "TLS record must stay inside response bounds");
|
assert!(
|
||||||
|
next <= response.len(),
|
||||||
|
"TLS record must stay inside response bounds"
|
||||||
|
);
|
||||||
if rtype == TLS_RECORD_APPLICATION {
|
if rtype == TLS_RECORD_APPLICATION {
|
||||||
app_records += 1;
|
app_records += 1;
|
||||||
if first_app_payload.is_none() {
|
if first_app_payload.is_none() {
|
||||||
@@ -2146,7 +2218,9 @@ fn server_hello_ignores_oversized_alpn_and_still_caps_ticket_tail() {
|
|||||||
}
|
}
|
||||||
pos = next;
|
pos = next;
|
||||||
}
|
}
|
||||||
let marker = [0x00u8, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, b'x', b'x', b'x', b'x'];
|
let marker = [
|
||||||
|
0x00u8, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, b'x', b'x', b'x', b'x',
|
||||||
|
];
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app_records, 5,
|
app_records, 5,
|
||||||
@@ -2310,13 +2384,13 @@ fn light_fuzz_tls_header_classifier_and_parser_policy_consistency() {
|
|||||||
&& header[1] == 0x03
|
&& header[1] == 0x03
|
||||||
&& (header[2] == 0x01 || header[2] == 0x03);
|
&& (header[2] == 0x01 || header[2] == 0x03);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classified,
|
classified, expected_classified,
|
||||||
expected_classified,
|
|
||||||
"classifier policy mismatch for header {header:02x?}"
|
"classifier policy mismatch for header {header:02x?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let parsed = parse_tls_record_header(&header);
|
let parsed = parse_tls_record_header(&header);
|
||||||
let expected_parsed = header[1] == 0x03 && (header[2] == 0x01 || header[2] == TLS_VERSION[1]);
|
let expected_parsed =
|
||||||
|
header[1] == 0x03 && (header[2] == 0x01 || header[2] == TLS_VERSION[1]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed.is_some(),
|
parsed.is_some(),
|
||||||
expected_parsed,
|
expected_parsed,
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use super::{MAX_TLS_CIPHERTEXT_SIZE, MAX_TLS_PLAINTEXT_SIZE, MIN_TLS_CLIENT_HELLO_SIZE};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_size_constants_match_rfc_8446() {
|
||||||
|
assert_eq!(MAX_TLS_PLAINTEXT_SIZE, 16_384);
|
||||||
|
assert_eq!(MAX_TLS_CIPHERTEXT_SIZE, 16_640);
|
||||||
|
|
||||||
|
assert!(MIN_TLS_CLIENT_HELLO_SIZE < 512);
|
||||||
|
assert!(MIN_TLS_CLIENT_HELLO_SIZE > 64);
|
||||||
|
assert!(MAX_TLS_CIPHERTEXT_SIZE > MAX_TLS_PLAINTEXT_SIZE);
|
||||||
|
}
|
||||||
+168
-59
@@ -5,11 +5,66 @@
|
|||||||
//! actually carries MTProto authentication data.
|
//! actually carries MTProto authentication data.
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
#![cfg_attr(not(test), forbid(clippy::undocumented_unsafe_blocks))]
|
||||||
|
#![cfg_attr(
|
||||||
|
not(test),
|
||||||
|
deny(
|
||||||
|
clippy::unwrap_used,
|
||||||
|
clippy::expect_used,
|
||||||
|
clippy::panic,
|
||||||
|
clippy::todo,
|
||||||
|
clippy::unimplemented,
|
||||||
|
clippy::correctness,
|
||||||
|
clippy::option_if_let_else,
|
||||||
|
clippy::or_fun_call,
|
||||||
|
clippy::branches_sharing_code,
|
||||||
|
clippy::single_option_map,
|
||||||
|
clippy::useless_let_if_seq,
|
||||||
|
clippy::redundant_locals,
|
||||||
|
clippy::cloned_ref_to_slice_refs,
|
||||||
|
unsafe_code,
|
||||||
|
clippy::await_holding_lock,
|
||||||
|
clippy::await_holding_refcell_ref,
|
||||||
|
clippy::debug_assert_with_mut_call,
|
||||||
|
clippy::macro_use_imports,
|
||||||
|
clippy::cast_ptr_alignment,
|
||||||
|
clippy::cast_lossless,
|
||||||
|
clippy::ptr_as_ptr,
|
||||||
|
clippy::large_stack_arrays,
|
||||||
|
clippy::same_functions_in_if_condition,
|
||||||
|
trivial_casts,
|
||||||
|
trivial_numeric_casts,
|
||||||
|
unused_extern_crates,
|
||||||
|
unused_import_braces,
|
||||||
|
rust_2018_idioms
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#![cfg_attr(
|
||||||
|
not(test),
|
||||||
|
allow(
|
||||||
|
clippy::use_self,
|
||||||
|
clippy::redundant_closure,
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::doc_markdown,
|
||||||
|
clippy::missing_const_for_fn,
|
||||||
|
clippy::unnecessary_operation,
|
||||||
|
clippy::redundant_pub_crate,
|
||||||
|
clippy::derive_partial_eq_without_eq,
|
||||||
|
clippy::type_complexity,
|
||||||
|
clippy::new_ret_no_self,
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_possible_wrap,
|
||||||
|
clippy::significant_drop_tightening,
|
||||||
|
clippy::significant_drop_in_scrutinee,
|
||||||
|
clippy::float_cmp,
|
||||||
|
clippy::nursery
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
|
||||||
use crate::crypto::{sha256_hmac, SecureRandom};
|
use super::constants::*;
|
||||||
|
use crate::crypto::{SecureRandom, sha256_hmac};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::error::ProxyError;
|
use crate::error::ProxyError;
|
||||||
use super::constants::*;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
use x25519_dalek::{X25519_BASEPOINT_BYTES, x25519};
|
use x25519_dalek::{X25519_BASEPOINT_BYTES, x25519};
|
||||||
@@ -31,7 +86,7 @@ pub const TLS_DIGEST_HALF_LEN: usize = 16;
|
|||||||
/// Operators with known clock-drifted clients should tune deployment config
|
/// Operators with known clock-drifted clients should tune deployment config
|
||||||
/// (for example replay-window policy) to match their environment.
|
/// (for example replay-window policy) to match their environment.
|
||||||
pub const TIME_SKEW_MIN: i64 = -2 * 60; // 2 minutes before
|
pub const TIME_SKEW_MIN: i64 = -2 * 60; // 2 minutes before
|
||||||
pub const TIME_SKEW_MAX: i64 = 2 * 60; // 2 minutes after
|
pub const TIME_SKEW_MAX: i64 = 2 * 60; // 2 minutes after
|
||||||
/// Maximum accepted boot-time timestamp (seconds) before skew checks are enforced.
|
/// Maximum accepted boot-time timestamp (seconds) before skew checks are enforced.
|
||||||
pub const BOOT_TIME_MAX_SECS: u32 = 7 * 24 * 60 * 60;
|
pub const BOOT_TIME_MAX_SECS: u32 = 7 * 24 * 60 * 60;
|
||||||
/// Hard cap for boot-time compatibility bypass to avoid oversized acceptance
|
/// Hard cap for boot-time compatibility bypass to avoid oversized acceptance
|
||||||
@@ -69,7 +124,6 @@ pub struct TlsValidation {
|
|||||||
/// Client digest for response generation
|
/// Client digest for response generation
|
||||||
pub digest: [u8; TLS_DIGEST_LEN],
|
pub digest: [u8; TLS_DIGEST_LEN],
|
||||||
/// Timestamp extracted from digest
|
/// Timestamp extracted from digest
|
||||||
|
|
||||||
pub timestamp: u32,
|
pub timestamp: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +145,8 @@ impl TlsExtensionBuilder {
|
|||||||
/// Add Key Share extension with X25519 key
|
/// Add Key Share extension with X25519 key
|
||||||
fn add_key_share(&mut self, public_key: &[u8; 32]) -> &mut Self {
|
fn add_key_share(&mut self, public_key: &[u8; 32]) -> &mut Self {
|
||||||
// Extension type: key_share (0x0033)
|
// Extension type: key_share (0x0033)
|
||||||
self.extensions.extend_from_slice(&extension_type::KEY_SHARE.to_be_bytes());
|
self.extensions
|
||||||
|
.extend_from_slice(&extension_type::KEY_SHARE.to_be_bytes());
|
||||||
|
|
||||||
// Key share entry: curve (2) + key_len (2) + key (32) = 36 bytes
|
// Key share entry: curve (2) + key_len (2) + key (32) = 36 bytes
|
||||||
// Extension data length
|
// Extension data length
|
||||||
@@ -99,7 +154,8 @@ impl TlsExtensionBuilder {
|
|||||||
self.extensions.extend_from_slice(&entry_len.to_be_bytes());
|
self.extensions.extend_from_slice(&entry_len.to_be_bytes());
|
||||||
|
|
||||||
// Named curve: x25519
|
// Named curve: x25519
|
||||||
self.extensions.extend_from_slice(&named_curve::X25519.to_be_bytes());
|
self.extensions
|
||||||
|
.extend_from_slice(&named_curve::X25519.to_be_bytes());
|
||||||
|
|
||||||
// Key length
|
// Key length
|
||||||
self.extensions.extend_from_slice(&(32u16).to_be_bytes());
|
self.extensions.extend_from_slice(&(32u16).to_be_bytes());
|
||||||
@@ -113,7 +169,8 @@ impl TlsExtensionBuilder {
|
|||||||
/// Add Supported Versions extension
|
/// Add Supported Versions extension
|
||||||
fn add_supported_versions(&mut self, version: u16) -> &mut Self {
|
fn add_supported_versions(&mut self, version: u16) -> &mut Self {
|
||||||
// Extension type: supported_versions (0x002b)
|
// Extension type: supported_versions (0x002b)
|
||||||
self.extensions.extend_from_slice(&extension_type::SUPPORTED_VERSIONS.to_be_bytes());
|
self.extensions
|
||||||
|
.extend_from_slice(&extension_type::SUPPORTED_VERSIONS.to_be_bytes());
|
||||||
|
|
||||||
// Extension data: length (2) + version (2)
|
// Extension data: length (2) + version (2)
|
||||||
self.extensions.extend_from_slice(&(2u16).to_be_bytes());
|
self.extensions.extend_from_slice(&(2u16).to_be_bytes());
|
||||||
@@ -125,12 +182,13 @@ impl TlsExtensionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build final extensions with length prefix
|
/// Build final extensions with length prefix
|
||||||
|
|
||||||
fn build(self) -> Vec<u8> {
|
fn build(self) -> Vec<u8> {
|
||||||
|
let Ok(len) = u16::try_from(self.extensions.len()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
let mut result = Vec::with_capacity(2 + self.extensions.len());
|
let mut result = Vec::with_capacity(2 + self.extensions.len());
|
||||||
|
|
||||||
// Extensions length (2 bytes)
|
// Extensions length (2 bytes)
|
||||||
let len = self.extensions.len() as u16;
|
|
||||||
result.extend_from_slice(&len.to_be_bytes());
|
result.extend_from_slice(&len.to_be_bytes());
|
||||||
|
|
||||||
// Extensions data
|
// Extensions data
|
||||||
@@ -140,7 +198,6 @@ impl TlsExtensionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get current extensions without length prefix (for calculation)
|
/// Get current extensions without length prefix (for calculation)
|
||||||
|
|
||||||
fn as_bytes(&self) -> &[u8] {
|
fn as_bytes(&self) -> &[u8] {
|
||||||
&self.extensions
|
&self.extensions
|
||||||
}
|
}
|
||||||
@@ -186,8 +243,13 @@ impl ServerHelloBuilder {
|
|||||||
|
|
||||||
/// Build ServerHello message (without record header)
|
/// Build ServerHello message (without record header)
|
||||||
fn build_message(&self) -> Vec<u8> {
|
fn build_message(&self) -> Vec<u8> {
|
||||||
|
let Ok(session_id_len) = u8::try_from(self.session_id.len()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
let extensions = self.extensions.extensions.clone();
|
let extensions = self.extensions.extensions.clone();
|
||||||
let extensions_len = extensions.len() as u16;
|
let Ok(extensions_len) = u16::try_from(extensions.len()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate total length
|
// Calculate total length
|
||||||
let body_len = 2 + // version
|
let body_len = 2 + // version
|
||||||
@@ -196,6 +258,9 @@ impl ServerHelloBuilder {
|
|||||||
2 + // cipher suite
|
2 + // cipher suite
|
||||||
1 + // compression
|
1 + // compression
|
||||||
2 + extensions.len(); // extensions length + data
|
2 + extensions.len(); // extensions length + data
|
||||||
|
if body_len > 0x00ff_ffff {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
let mut message = Vec::with_capacity(4 + body_len);
|
let mut message = Vec::with_capacity(4 + body_len);
|
||||||
|
|
||||||
@@ -203,7 +268,10 @@ impl ServerHelloBuilder {
|
|||||||
message.push(0x02); // ServerHello message type
|
message.push(0x02); // ServerHello message type
|
||||||
|
|
||||||
// 3-byte length
|
// 3-byte length
|
||||||
let len_bytes = (body_len as u32).to_be_bytes();
|
let Ok(body_len_u32) = u32::try_from(body_len) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let len_bytes = body_len_u32.to_be_bytes();
|
||||||
message.extend_from_slice(&len_bytes[1..4]);
|
message.extend_from_slice(&len_bytes[1..4]);
|
||||||
|
|
||||||
// Server version (TLS 1.2 in header, actual version in extension)
|
// Server version (TLS 1.2 in header, actual version in extension)
|
||||||
@@ -213,7 +281,7 @@ impl ServerHelloBuilder {
|
|||||||
message.extend_from_slice(&self.random);
|
message.extend_from_slice(&self.random);
|
||||||
|
|
||||||
// Session ID
|
// Session ID
|
||||||
message.push(self.session_id.len() as u8);
|
message.push(session_id_len);
|
||||||
message.extend_from_slice(&self.session_id);
|
message.extend_from_slice(&self.session_id);
|
||||||
|
|
||||||
// Cipher suite
|
// Cipher suite
|
||||||
@@ -234,13 +302,19 @@ impl ServerHelloBuilder {
|
|||||||
/// Build complete ServerHello TLS record
|
/// Build complete ServerHello TLS record
|
||||||
fn build_record(&self) -> Vec<u8> {
|
fn build_record(&self) -> Vec<u8> {
|
||||||
let message = self.build_message();
|
let message = self.build_message();
|
||||||
|
if message.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let Ok(message_len) = u16::try_from(message.len()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
let mut record = Vec::with_capacity(5 + message.len());
|
let mut record = Vec::with_capacity(5 + message.len());
|
||||||
|
|
||||||
// TLS record header
|
// TLS record header
|
||||||
record.push(TLS_RECORD_HANDSHAKE);
|
record.push(TLS_RECORD_HANDSHAKE);
|
||||||
record.extend_from_slice(&TLS_VERSION);
|
record.extend_from_slice(&TLS_VERSION);
|
||||||
record.extend_from_slice(&(message.len() as u16).to_be_bytes());
|
record.extend_from_slice(&message_len.to_be_bytes());
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
record.extend_from_slice(&message);
|
record.extend_from_slice(&message);
|
||||||
@@ -256,7 +330,6 @@ impl ServerHelloBuilder {
|
|||||||
/// Returns validation result if a matching user is found.
|
/// Returns validation result if a matching user is found.
|
||||||
/// The result **must** be used — ignoring it silently bypasses authentication.
|
/// The result **must** be used — ignoring it silently bypasses authentication.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
||||||
pub fn validate_tls_handshake(
|
pub fn validate_tls_handshake(
|
||||||
handshake: &[u8],
|
handshake: &[u8],
|
||||||
secrets: &[(String, Vec<u8>)],
|
secrets: &[(String, Vec<u8>)],
|
||||||
@@ -320,7 +393,6 @@ fn system_time_to_unix_secs(now: SystemTime) -> Option<i64> {
|
|||||||
i64::try_from(d.as_secs()).ok()
|
i64::try_from(d.as_secs()).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn validate_tls_handshake_at_time(
|
fn validate_tls_handshake_at_time(
|
||||||
handshake: &[u8],
|
handshake: &[u8],
|
||||||
secrets: &[(String, Vec<u8>)],
|
secrets: &[(String, Vec<u8>)],
|
||||||
@@ -450,7 +522,7 @@ pub fn build_server_hello(
|
|||||||
new_session_tickets: u8,
|
new_session_tickets: u8,
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
const MIN_APP_DATA: usize = 64;
|
const MIN_APP_DATA: usize = 64;
|
||||||
const MAX_APP_DATA: usize = 16640; // RFC 8446 §5.2 upper bound
|
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||||
let fake_cert_len = fake_cert_len.clamp(MIN_APP_DATA, MAX_APP_DATA);
|
let fake_cert_len = fake_cert_len.clamp(MIN_APP_DATA, MAX_APP_DATA);
|
||||||
let x25519_key = gen_fake_x25519_key(rng);
|
let x25519_key = gen_fake_x25519_key(rng);
|
||||||
|
|
||||||
@@ -463,15 +535,20 @@ pub fn build_server_hello(
|
|||||||
// Build Change Cipher Spec record
|
// Build Change Cipher Spec record
|
||||||
let change_cipher_spec = [
|
let change_cipher_spec = [
|
||||||
TLS_RECORD_CHANGE_CIPHER,
|
TLS_RECORD_CHANGE_CIPHER,
|
||||||
TLS_VERSION[0], TLS_VERSION[1],
|
TLS_VERSION[0],
|
||||||
0x00, 0x01, // length = 1
|
TLS_VERSION[1],
|
||||||
0x01, // CCS byte
|
0x00,
|
||||||
|
0x01, // length = 1
|
||||||
|
0x01, // CCS byte
|
||||||
];
|
];
|
||||||
|
|
||||||
// Build first encrypted flight mimic as opaque ApplicationData bytes.
|
// Build first encrypted flight mimic as opaque ApplicationData bytes.
|
||||||
// Embed a compact EncryptedExtensions-like ALPN block when selected.
|
// Embed a compact EncryptedExtensions-like ALPN block when selected.
|
||||||
let mut fake_cert = Vec::with_capacity(fake_cert_len);
|
let mut fake_cert = Vec::with_capacity(fake_cert_len);
|
||||||
if let Some(proto) = alpn.as_ref().filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize) {
|
if let Some(proto) = alpn
|
||||||
|
.as_ref()
|
||||||
|
.filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize)
|
||||||
|
{
|
||||||
let proto_list_len = 1usize + proto.len();
|
let proto_list_len = 1usize + proto.len();
|
||||||
let ext_data_len = 2usize + proto_list_len;
|
let ext_data_len = 2usize + proto_list_len;
|
||||||
let marker_len = 4usize + ext_data_len;
|
let marker_len = 4usize + ext_data_len;
|
||||||
@@ -515,7 +592,10 @@ pub fn build_server_hello(
|
|||||||
|
|
||||||
// Combine all records
|
// Combine all records
|
||||||
let mut response = Vec::with_capacity(
|
let mut response = Vec::with_capacity(
|
||||||
server_hello.len() + change_cipher_spec.len() + app_data_record.len() + tickets.iter().map(|r| r.len()).sum::<usize>()
|
server_hello.len()
|
||||||
|
+ change_cipher_spec.len()
|
||||||
|
+ app_data_record.len()
|
||||||
|
+ tickets.iter().map(|r| r.len()).sum::<usize>(),
|
||||||
);
|
);
|
||||||
response.extend_from_slice(&server_hello);
|
response.extend_from_slice(&server_hello);
|
||||||
response.extend_from_slice(&change_cipher_spec);
|
response.extend_from_slice(&change_cipher_spec);
|
||||||
@@ -532,8 +612,7 @@ pub fn build_server_hello(
|
|||||||
|
|
||||||
// Insert computed digest into ServerHello
|
// Insert computed digest into ServerHello
|
||||||
// Position: record header (5) + message type (1) + length (3) + version (2) = 11
|
// Position: record header (5) + message type (1) + length (3) + version (2) = 11
|
||||||
response[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
response[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&response_digest);
|
||||||
.copy_from_slice(&response_digest);
|
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
@@ -588,6 +667,9 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option<String> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut saw_sni_extension = false;
|
||||||
|
let mut extracted_sni = None;
|
||||||
|
|
||||||
while pos + 4 <= ext_end {
|
while pos + 4 <= ext_end {
|
||||||
let etype = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]);
|
let etype = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]);
|
||||||
let elen = u16::from_be_bytes([handshake[pos + 2], handshake[pos + 3]]) as usize;
|
let elen = u16::from_be_bytes([handshake[pos + 2], handshake[pos + 3]]) as usize;
|
||||||
@@ -595,6 +677,12 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option<String> {
|
|||||||
if pos + elen > ext_end {
|
if pos + elen > ext_end {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if etype == 0x0000 {
|
||||||
|
if saw_sni_extension {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
saw_sni_extension = true;
|
||||||
|
}
|
||||||
if etype == 0x0000 && elen >= 5 {
|
if etype == 0x0000 && elen >= 5 {
|
||||||
// server_name extension
|
// server_name extension
|
||||||
let list_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
let list_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||||
@@ -602,17 +690,19 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option<String> {
|
|||||||
let sn_end = std::cmp::min(sn_pos + list_len, pos + elen);
|
let sn_end = std::cmp::min(sn_pos + list_len, pos + elen);
|
||||||
while sn_pos + 3 <= sn_end {
|
while sn_pos + 3 <= sn_end {
|
||||||
let name_type = handshake[sn_pos];
|
let name_type = handshake[sn_pos];
|
||||||
let name_len = u16::from_be_bytes([handshake[sn_pos + 1], handshake[sn_pos + 2]]) as usize;
|
let name_len =
|
||||||
|
u16::from_be_bytes([handshake[sn_pos + 1], handshake[sn_pos + 2]]) as usize;
|
||||||
sn_pos += 3;
|
sn_pos += 3;
|
||||||
if sn_pos + name_len > sn_end {
|
if sn_pos + name_len > sn_end {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if name_type == 0 && name_len > 0
|
if name_type == 0
|
||||||
|
&& name_len > 0
|
||||||
&& let Ok(host) = std::str::from_utf8(&handshake[sn_pos..sn_pos + name_len])
|
&& let Ok(host) = std::str::from_utf8(&handshake[sn_pos..sn_pos + name_len])
|
||||||
|
&& is_valid_sni_hostname(host)
|
||||||
{
|
{
|
||||||
if is_valid_sni_hostname(host) {
|
extracted_sni = Some(host.to_string());
|
||||||
return Some(host.to_string());
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sn_pos += name_len;
|
sn_pos += name_len;
|
||||||
}
|
}
|
||||||
@@ -620,7 +710,7 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option<String> {
|
|||||||
pos += elen;
|
pos += elen;
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
extracted_sni
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_valid_sni_hostname(host: &str) -> bool {
|
fn is_valid_sni_hostname(host: &str) -> bool {
|
||||||
@@ -669,35 +759,49 @@ pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
pos += 4; // type + len
|
pos += 4; // type + len
|
||||||
pos += 2 + 32; // version + random
|
pos += 2 + 32; // version + random
|
||||||
if pos >= handshake.len() { return Vec::new(); }
|
if pos >= handshake.len() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
let session_id_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
let session_id_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
||||||
pos += 1 + session_id_len;
|
pos += 1 + session_id_len;
|
||||||
if pos + 2 > handshake.len() { return Vec::new(); }
|
if pos + 2 > handshake.len() {
|
||||||
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||||
pos += 2 + cipher_len;
|
pos += 2 + cipher_len;
|
||||||
if pos >= handshake.len() { return Vec::new(); }
|
if pos >= handshake.len() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
let comp_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
let comp_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
||||||
pos += 1 + comp_len;
|
pos += 1 + comp_len;
|
||||||
if pos + 2 > handshake.len() { return Vec::new(); }
|
if pos + 2 > handshake.len() {
|
||||||
let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||||
pos += 2;
|
pos += 2;
|
||||||
let ext_end = pos + ext_len;
|
let ext_end = pos + ext_len;
|
||||||
if ext_end > handshake.len() { return Vec::new(); }
|
if ext_end > handshake.len() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
while pos + 4 <= ext_end {
|
while pos + 4 <= ext_end {
|
||||||
let etype = u16::from_be_bytes([handshake[pos], handshake[pos+1]]);
|
let etype = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]);
|
||||||
let elen = u16::from_be_bytes([handshake[pos+2], handshake[pos+3]]) as usize;
|
let elen = u16::from_be_bytes([handshake[pos + 2], handshake[pos + 3]]) as usize;
|
||||||
pos += 4;
|
pos += 4;
|
||||||
if pos + elen > ext_end { break; }
|
if pos + elen > ext_end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
if etype == extension_type::ALPN && elen >= 3 {
|
if etype == extension_type::ALPN && elen >= 3 {
|
||||||
let list_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
let list_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||||
let mut lp = pos + 2;
|
let mut lp = pos + 2;
|
||||||
let list_end = (pos + 2).saturating_add(list_len).min(pos + elen);
|
let list_end = (pos + 2).saturating_add(list_len).min(pos + elen);
|
||||||
while lp < list_end {
|
while lp < list_end {
|
||||||
let plen = handshake[lp] as usize;
|
let plen = handshake[lp] as usize;
|
||||||
lp += 1;
|
lp += 1;
|
||||||
if lp + plen > list_end { break; }
|
if lp + plen > list_end {
|
||||||
out.push(handshake[lp..lp+plen].to_vec());
|
break;
|
||||||
|
}
|
||||||
|
out.push(handshake[lp..lp + plen].to_vec());
|
||||||
lp += plen;
|
lp += plen;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -707,7 +811,6 @@ pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Check if bytes look like a TLS ClientHello
|
/// Check if bytes look like a TLS ClientHello
|
||||||
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
||||||
if first_bytes.len() < 3 {
|
if first_bytes.len() < 3 {
|
||||||
@@ -721,7 +824,6 @@ pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse TLS record header, returns (record_type, length)
|
/// Parse TLS record header, returns (record_type, length)
|
||||||
|
|
||||||
pub fn parse_tls_record_header(header: &[u8; 5]) -> Option<(u8, u16)> {
|
pub fn parse_tls_record_header(header: &[u8; 5]) -> Option<(u8, u16)> {
|
||||||
let record_type = header[0];
|
let record_type = header[0];
|
||||||
let version = [header[1], header[2]];
|
let version = [header[1], header[2]];
|
||||||
@@ -766,25 +868,28 @@ fn validate_server_hello_structure(data: &[u8]) -> Result<(), ProxyError> {
|
|||||||
// Check record length
|
// Check record length
|
||||||
let record_len = u16::from_be_bytes([data[3], data[4]]) as usize;
|
let record_len = u16::from_be_bytes([data[3], data[4]]) as usize;
|
||||||
if data.len() < 5 + record_len {
|
if data.len() < 5 + record_len {
|
||||||
return Err(ProxyError::InvalidHandshake(
|
return Err(ProxyError::InvalidHandshake(format!(
|
||||||
format!("ServerHello record truncated: expected {}, got {}",
|
"ServerHello record truncated: expected {}, got {}",
|
||||||
5 + record_len, data.len())
|
5 + record_len,
|
||||||
));
|
data.len()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check message type
|
// Check message type
|
||||||
if data[5] != 0x02 {
|
if data[5] != 0x02 {
|
||||||
return Err(ProxyError::InvalidHandshake(
|
return Err(ProxyError::InvalidHandshake(format!(
|
||||||
format!("Expected ServerHello (0x02), got 0x{:02x}", data[5])
|
"Expected ServerHello (0x02), got 0x{:02x}",
|
||||||
));
|
data[5]
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse message length
|
// Parse message length
|
||||||
let msg_len = u32::from_be_bytes([0, data[6], data[7], data[8]]) as usize;
|
let msg_len = u32::from_be_bytes([0, data[6], data[7], data[8]]) as usize;
|
||||||
if msg_len + 4 != record_len {
|
if msg_len + 4 != record_len {
|
||||||
return Err(ProxyError::InvalidHandshake(
|
return Err(ProxyError::InvalidHandshake(format!(
|
||||||
format!("Message length mismatch: {} + 4 != {}", msg_len, record_len)
|
"Message length mismatch: {} + 4 != {}",
|
||||||
));
|
msg_len, record_len
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -796,7 +901,7 @@ fn validate_server_hello_structure(data: &[u8]) -> Result<(), ProxyError> {
|
|||||||
/// Using `static_assertions` ensures these can never silently break across
|
/// Using `static_assertions` ensures these can never silently break across
|
||||||
/// refactors without a compile error.
|
/// refactors without a compile error.
|
||||||
mod compile_time_security_checks {
|
mod compile_time_security_checks {
|
||||||
use super::{TLS_DIGEST_LEN, TLS_DIGEST_HALF_LEN};
|
use super::{TLS_DIGEST_HALF_LEN, TLS_DIGEST_LEN};
|
||||||
use static_assertions::const_assert;
|
use static_assertions::const_assert;
|
||||||
|
|
||||||
// The digest must be exactly one SHA-256 output.
|
// The digest must be exactly one SHA-256 output.
|
||||||
@@ -814,13 +919,17 @@ mod compile_time_security_checks {
|
|||||||
// ============= Security-focused regression tests =============
|
// ============= Security-focused regression tests =============
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tls_security_tests.rs"]
|
#[path = "tests/tls_security_tests.rs"]
|
||||||
mod security_tests;
|
mod security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tls_adversarial_tests.rs"]
|
#[path = "tests/tls_adversarial_tests.rs"]
|
||||||
mod adversarial_tests;
|
mod adversarial_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tls_fuzz_security_tests.rs"]
|
#[path = "tests/tls_fuzz_security_tests.rs"]
|
||||||
mod fuzz_security_tests;
|
mod fuzz_security_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/tls_length_cast_hardening_security_tests.rs"]
|
||||||
|
mod length_cast_hardening_security_tests;
|
||||||
|
|||||||
@@ -0,0 +1,408 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
// Adaptive buffer policy is staged and retained for deterministic rollout.
|
||||||
|
// Keep definitions compiled for compatibility and security test scaffolding.
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use std::cmp::max;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
const EMA_ALPHA: f64 = 0.2;
|
||||||
|
const PROFILE_TTL: Duration = Duration::from_secs(300);
|
||||||
|
const THROUGHPUT_UP_BPS: f64 = 8_000_000.0;
|
||||||
|
const THROUGHPUT_DOWN_BPS: f64 = 2_000_000.0;
|
||||||
|
const RATIO_CONFIRM_THRESHOLD: f64 = 1.12;
|
||||||
|
const TIER1_HOLD_TICKS: u32 = 8;
|
||||||
|
const TIER2_HOLD_TICKS: u32 = 4;
|
||||||
|
const QUIET_DEMOTE_TICKS: u32 = 480;
|
||||||
|
const HARD_COOLDOWN_TICKS: u32 = 20;
|
||||||
|
const HARD_PENDING_THRESHOLD: u32 = 3;
|
||||||
|
const HARD_PARTIAL_RATIO_THRESHOLD: f64 = 0.25;
|
||||||
|
const DIRECT_C2S_CAP_BYTES: usize = 128 * 1024;
|
||||||
|
const DIRECT_S2C_CAP_BYTES: usize = 512 * 1024;
|
||||||
|
const ME_FRAMES_CAP: usize = 96;
|
||||||
|
const ME_BYTES_CAP: usize = 384 * 1024;
|
||||||
|
const ME_DELAY_MIN_US: u64 = 150;
|
||||||
|
const MAX_USER_PROFILES_ENTRIES: usize = 50_000;
|
||||||
|
const MAX_USER_KEY_BYTES: usize = 512;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum AdaptiveTier {
|
||||||
|
Base = 0,
|
||||||
|
Tier1 = 1,
|
||||||
|
Tier2 = 2,
|
||||||
|
Tier3 = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdaptiveTier {
|
||||||
|
pub fn promote(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Base => Self::Tier1,
|
||||||
|
Self::Tier1 => Self::Tier2,
|
||||||
|
Self::Tier2 => Self::Tier3,
|
||||||
|
Self::Tier3 => Self::Tier3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn demote(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Base => Self::Base,
|
||||||
|
Self::Tier1 => Self::Base,
|
||||||
|
Self::Tier2 => Self::Tier1,
|
||||||
|
Self::Tier3 => Self::Tier2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ratio(self) -> (usize, usize) {
|
||||||
|
match self {
|
||||||
|
Self::Base => (1, 1),
|
||||||
|
Self::Tier1 => (5, 4),
|
||||||
|
Self::Tier2 => (3, 2),
|
||||||
|
Self::Tier3 => (2, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_u8(self) -> u8 {
|
||||||
|
self as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TierTransitionReason {
|
||||||
|
SoftConfirmed,
|
||||||
|
HardPressure,
|
||||||
|
QuietDemotion,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct TierTransition {
|
||||||
|
pub from: AdaptiveTier,
|
||||||
|
pub to: AdaptiveTier,
|
||||||
|
pub reason: TierTransitionReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct RelaySignalSample {
|
||||||
|
pub c2s_bytes: u64,
|
||||||
|
pub s2c_requested_bytes: u64,
|
||||||
|
pub s2c_written_bytes: u64,
|
||||||
|
pub s2c_write_ops: u64,
|
||||||
|
pub s2c_partial_writes: u64,
|
||||||
|
pub s2c_consecutive_pending_writes: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SessionAdaptiveController {
|
||||||
|
tier: AdaptiveTier,
|
||||||
|
max_tier_seen: AdaptiveTier,
|
||||||
|
throughput_ema_bps: f64,
|
||||||
|
incoming_ema_bps: f64,
|
||||||
|
outgoing_ema_bps: f64,
|
||||||
|
tier1_hold_ticks: u32,
|
||||||
|
tier2_hold_ticks: u32,
|
||||||
|
quiet_ticks: u32,
|
||||||
|
hard_cooldown_ticks: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionAdaptiveController {
|
||||||
|
pub fn new(initial_tier: AdaptiveTier) -> Self {
|
||||||
|
Self {
|
||||||
|
tier: initial_tier,
|
||||||
|
max_tier_seen: initial_tier,
|
||||||
|
throughput_ema_bps: 0.0,
|
||||||
|
incoming_ema_bps: 0.0,
|
||||||
|
outgoing_ema_bps: 0.0,
|
||||||
|
tier1_hold_ticks: 0,
|
||||||
|
tier2_hold_ticks: 0,
|
||||||
|
quiet_ticks: 0,
|
||||||
|
hard_cooldown_ticks: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_tier_seen(&self) -> AdaptiveTier {
|
||||||
|
self.max_tier_seen
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn observe(&mut self, sample: RelaySignalSample, tick_secs: f64) -> Option<TierTransition> {
|
||||||
|
if tick_secs <= f64::EPSILON {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hard_cooldown_ticks > 0 {
|
||||||
|
self.hard_cooldown_ticks -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let c2s_bps = (sample.c2s_bytes as f64 * 8.0) / tick_secs;
|
||||||
|
let incoming_bps = (sample.s2c_requested_bytes as f64 * 8.0) / tick_secs;
|
||||||
|
let outgoing_bps = (sample.s2c_written_bytes as f64 * 8.0) / tick_secs;
|
||||||
|
let throughput = c2s_bps.max(outgoing_bps);
|
||||||
|
|
||||||
|
self.throughput_ema_bps = ema(self.throughput_ema_bps, throughput);
|
||||||
|
self.incoming_ema_bps = ema(self.incoming_ema_bps, incoming_bps);
|
||||||
|
self.outgoing_ema_bps = ema(self.outgoing_ema_bps, outgoing_bps);
|
||||||
|
|
||||||
|
let tier1_now = self.throughput_ema_bps >= THROUGHPUT_UP_BPS;
|
||||||
|
if tier1_now {
|
||||||
|
self.tier1_hold_ticks = self.tier1_hold_ticks.saturating_add(1);
|
||||||
|
} else {
|
||||||
|
self.tier1_hold_ticks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ratio = if self.outgoing_ema_bps <= f64::EPSILON {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
self.incoming_ema_bps / self.outgoing_ema_bps
|
||||||
|
};
|
||||||
|
let tier2_now = ratio >= RATIO_CONFIRM_THRESHOLD;
|
||||||
|
if tier2_now {
|
||||||
|
self.tier2_hold_ticks = self.tier2_hold_ticks.saturating_add(1);
|
||||||
|
} else {
|
||||||
|
self.tier2_hold_ticks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let partial_ratio = if sample.s2c_write_ops == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
sample.s2c_partial_writes as f64 / sample.s2c_write_ops as f64
|
||||||
|
};
|
||||||
|
let hard_now = sample.s2c_consecutive_pending_writes >= HARD_PENDING_THRESHOLD
|
||||||
|
|| partial_ratio >= HARD_PARTIAL_RATIO_THRESHOLD;
|
||||||
|
|
||||||
|
if hard_now && self.hard_cooldown_ticks == 0 {
|
||||||
|
return self.promote(TierTransitionReason::HardPressure, HARD_COOLDOWN_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.tier1_hold_ticks >= TIER1_HOLD_TICKS && self.tier2_hold_ticks >= TIER2_HOLD_TICKS {
|
||||||
|
return self.promote(TierTransitionReason::SoftConfirmed, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let demote_candidate =
|
||||||
|
self.throughput_ema_bps < THROUGHPUT_DOWN_BPS && !tier2_now && !hard_now;
|
||||||
|
if demote_candidate {
|
||||||
|
self.quiet_ticks = self.quiet_ticks.saturating_add(1);
|
||||||
|
if self.quiet_ticks >= QUIET_DEMOTE_TICKS {
|
||||||
|
self.quiet_ticks = 0;
|
||||||
|
return self.demote(TierTransitionReason::QuietDemotion);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.quiet_ticks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn promote(
|
||||||
|
&mut self,
|
||||||
|
reason: TierTransitionReason,
|
||||||
|
hard_cooldown_ticks: u32,
|
||||||
|
) -> Option<TierTransition> {
|
||||||
|
let from = self.tier;
|
||||||
|
let to = from.promote();
|
||||||
|
if from == to {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.tier = to;
|
||||||
|
self.max_tier_seen = max(self.max_tier_seen, to);
|
||||||
|
self.hard_cooldown_ticks = hard_cooldown_ticks;
|
||||||
|
self.tier1_hold_ticks = 0;
|
||||||
|
self.tier2_hold_ticks = 0;
|
||||||
|
self.quiet_ticks = 0;
|
||||||
|
Some(TierTransition { from, to, reason })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demote(&mut self, reason: TierTransitionReason) -> Option<TierTransition> {
|
||||||
|
let from = self.tier;
|
||||||
|
let to = from.demote();
|
||||||
|
if from == to {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.tier = to;
|
||||||
|
self.tier1_hold_ticks = 0;
|
||||||
|
self.tier2_hold_ticks = 0;
|
||||||
|
Some(TierTransition { from, to, reason })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct UserAdaptiveProfile {
|
||||||
|
tier: AdaptiveTier,
|
||||||
|
seen_at: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn profiles() -> &'static DashMap<String, UserAdaptiveProfile> {
|
||||||
|
static USER_PROFILES: OnceLock<DashMap<String, UserAdaptiveProfile>> = OnceLock::new();
|
||||||
|
USER_PROFILES.get_or_init(DashMap::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seed_tier_for_user(user: &str) -> AdaptiveTier {
|
||||||
|
if user.len() > MAX_USER_KEY_BYTES {
|
||||||
|
return AdaptiveTier::Base;
|
||||||
|
}
|
||||||
|
let now = Instant::now();
|
||||||
|
if let Some(entry) = profiles().get(user) {
|
||||||
|
let value = *entry.value();
|
||||||
|
drop(entry);
|
||||||
|
if now.saturating_duration_since(value.seen_at) <= PROFILE_TTL {
|
||||||
|
return value.tier;
|
||||||
|
}
|
||||||
|
profiles().remove_if(user, |_, v| {
|
||||||
|
now.saturating_duration_since(v.seen_at) > PROFILE_TTL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
AdaptiveTier::Base
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_user_tier(user: &str, tier: AdaptiveTier) {
|
||||||
|
if user.len() > MAX_USER_KEY_BYTES {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut was_vacant = false;
|
||||||
|
match profiles().entry(user.to_string()) {
|
||||||
|
dashmap::mapref::entry::Entry::Occupied(mut entry) => {
|
||||||
|
let existing = *entry.get();
|
||||||
|
let effective = if now.saturating_duration_since(existing.seen_at) > PROFILE_TTL {
|
||||||
|
tier
|
||||||
|
} else {
|
||||||
|
max(existing.tier, tier)
|
||||||
|
};
|
||||||
|
entry.insert(UserAdaptiveProfile {
|
||||||
|
tier: effective,
|
||||||
|
seen_at: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dashmap::mapref::entry::Entry::Vacant(slot) => {
|
||||||
|
slot.insert(UserAdaptiveProfile { tier, seen_at: now });
|
||||||
|
was_vacant = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if was_vacant && profiles().len() > MAX_USER_PROFILES_ENTRIES {
|
||||||
|
profiles().retain(|_, v| now.saturating_duration_since(v.seen_at) <= PROFILE_TTL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn direct_copy_buffers_for_tier(
|
||||||
|
tier: AdaptiveTier,
|
||||||
|
base_c2s: usize,
|
||||||
|
base_s2c: usize,
|
||||||
|
) -> (usize, usize) {
|
||||||
|
let (num, den) = tier.ratio();
|
||||||
|
(
|
||||||
|
scale(base_c2s, num, den, DIRECT_C2S_CAP_BYTES),
|
||||||
|
scale(base_s2c, num, den, DIRECT_S2C_CAP_BYTES),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn me_flush_policy_for_tier(
|
||||||
|
tier: AdaptiveTier,
|
||||||
|
base_frames: usize,
|
||||||
|
base_bytes: usize,
|
||||||
|
base_delay: Duration,
|
||||||
|
) -> (usize, usize, Duration) {
|
||||||
|
let (num, den) = tier.ratio();
|
||||||
|
let frames = scale(base_frames, num, den, ME_FRAMES_CAP).max(1);
|
||||||
|
let bytes = scale(base_bytes, num, den, ME_BYTES_CAP).max(4096);
|
||||||
|
let delay_us = base_delay.as_micros() as u64;
|
||||||
|
let adjusted_delay_us = match tier {
|
||||||
|
AdaptiveTier::Base => delay_us,
|
||||||
|
AdaptiveTier::Tier1 => (delay_us.saturating_mul(7)).saturating_div(10),
|
||||||
|
AdaptiveTier::Tier2 => delay_us.saturating_div(2),
|
||||||
|
AdaptiveTier::Tier3 => (delay_us.saturating_mul(3)).saturating_div(10),
|
||||||
|
}
|
||||||
|
.max(ME_DELAY_MIN_US)
|
||||||
|
.min(delay_us.max(ME_DELAY_MIN_US));
|
||||||
|
(frames, bytes, Duration::from_micros(adjusted_delay_us))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ema(prev: f64, value: f64) -> f64 {
|
||||||
|
if prev <= f64::EPSILON {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
(prev * (1.0 - EMA_ALPHA)) + (value * EMA_ALPHA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scale(base: usize, numerator: usize, denominator: usize, cap: usize) -> usize {
|
||||||
|
let scaled = base
|
||||||
|
.saturating_mul(numerator)
|
||||||
|
.saturating_div(denominator.max(1));
|
||||||
|
scaled.min(cap).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/adaptive_buffers_security_tests.rs"]
|
||||||
|
mod adaptive_buffers_security_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/adaptive_buffers_record_race_security_tests.rs"]
|
||||||
|
mod adaptive_buffers_record_race_security_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn sample(
|
||||||
|
c2s_bytes: u64,
|
||||||
|
s2c_requested_bytes: u64,
|
||||||
|
s2c_written_bytes: u64,
|
||||||
|
s2c_write_ops: u64,
|
||||||
|
s2c_partial_writes: u64,
|
||||||
|
s2c_consecutive_pending_writes: u32,
|
||||||
|
) -> RelaySignalSample {
|
||||||
|
RelaySignalSample {
|
||||||
|
c2s_bytes,
|
||||||
|
s2c_requested_bytes,
|
||||||
|
s2c_written_bytes,
|
||||||
|
s2c_write_ops,
|
||||||
|
s2c_partial_writes,
|
||||||
|
s2c_consecutive_pending_writes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_soft_promotion_requires_tier1_and_tier2() {
|
||||||
|
let mut ctrl = SessionAdaptiveController::new(AdaptiveTier::Base);
|
||||||
|
let tick_secs = 0.25;
|
||||||
|
let mut promoted = None;
|
||||||
|
for _ in 0..8 {
|
||||||
|
promoted = ctrl.observe(
|
||||||
|
sample(
|
||||||
|
300_000, // ~9.6 Mbps
|
||||||
|
320_000, // incoming > outgoing to confirm tier2
|
||||||
|
250_000, 10, 0, 0,
|
||||||
|
),
|
||||||
|
tick_secs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition = promoted.expect("expected soft promotion");
|
||||||
|
assert_eq!(transition.from, AdaptiveTier::Base);
|
||||||
|
assert_eq!(transition.to, AdaptiveTier::Tier1);
|
||||||
|
assert_eq!(transition.reason, TierTransitionReason::SoftConfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hard_promotion_on_pending_pressure() {
|
||||||
|
let mut ctrl = SessionAdaptiveController::new(AdaptiveTier::Base);
|
||||||
|
let transition = ctrl
|
||||||
|
.observe(sample(10_000, 20_000, 10_000, 4, 1, 3), 0.25)
|
||||||
|
.expect("expected hard promotion");
|
||||||
|
assert_eq!(transition.reason, TierTransitionReason::HardPressure);
|
||||||
|
assert_eq!(transition.to, AdaptiveTier::Tier1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quiet_demotion_is_slow_and_stepwise() {
|
||||||
|
let mut ctrl = SessionAdaptiveController::new(AdaptiveTier::Tier2);
|
||||||
|
let mut demotion = None;
|
||||||
|
for _ in 0..QUIET_DEMOTE_TICKS {
|
||||||
|
demotion = ctrl.observe(sample(1, 1, 1, 1, 0, 0), 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition = demotion.expect("expected quiet demotion");
|
||||||
|
assert_eq!(transition.from, AdaptiveTier::Tier2);
|
||||||
|
assert_eq!(transition.to, AdaptiveTier::Tier1);
|
||||||
|
assert_eq!(transition.reason, TierTransitionReason::QuietDemotion);
|
||||||
|
}
|
||||||
|
}
|
||||||
+895
-191
File diff suppressed because it is too large
Load Diff
@@ -1,109 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::ProxyConfig;
|
|
||||||
use crate::stats::Stats;
|
|
||||||
use crate::ip_tracker::UserIpTracker;
|
|
||||||
use crate::error::ProxyError;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Priority 3: Massive Concurrency Stress (OWASP ASVS 5.1.6)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn client_stress_10k_connections_limit_strict() {
|
|
||||||
let user = "stress-user";
|
|
||||||
let limit = 512;
|
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.access.user_max_tcp_conns.insert(user.to_string(), limit);
|
|
||||||
|
|
||||||
let iterations = 1000;
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
|
|
||||||
for i in 0..iterations {
|
|
||||||
let stats = Arc::clone(&stats);
|
|
||||||
let ip_tracker = Arc::clone(&ip_tracker);
|
|
||||||
let config = config.clone();
|
|
||||||
let user_str = user.to_string();
|
|
||||||
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
let peer = SocketAddr::new(
|
|
||||||
IpAddr::V4(Ipv4Addr::new(127, 0, 0, (i % 254 + 1) as u8)),
|
|
||||||
10000 + (i % 1000) as u16,
|
|
||||||
);
|
|
||||||
|
|
||||||
match RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
&user_str,
|
|
||||||
&config,
|
|
||||||
stats,
|
|
||||||
peer,
|
|
||||||
ip_tracker,
|
|
||||||
).await {
|
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(ProxyError::ConnectionLimitExceeded { .. }) => Err(()),
|
|
||||||
Err(e) => panic!("Unexpected error: {:?}", e),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let results = futures::future::join_all(tasks).await;
|
|
||||||
let mut successes = 0;
|
|
||||||
let mut failures = 0;
|
|
||||||
let mut reservations = Vec::new();
|
|
||||||
|
|
||||||
for res in results {
|
|
||||||
match res.unwrap() {
|
|
||||||
Ok(r) => {
|
|
||||||
successes += 1;
|
|
||||||
reservations.push(r);
|
|
||||||
}
|
|
||||||
Err(_) => failures += 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(successes, limit, "Should allow exactly 'limit' connections");
|
|
||||||
assert_eq!(failures, iterations - limit, "Should fail the rest with LimitExceeded");
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), limit as u64);
|
|
||||||
|
|
||||||
drop(reservations);
|
|
||||||
|
|
||||||
ip_tracker.drain_cleanup_queue().await;
|
|
||||||
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 0, "Stats must converge to 0 after all drops");
|
|
||||||
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0, "IP tracker must converge to 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Priority 3: IP Tracker Race Stress
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn client_ip_tracker_race_condition_stress() {
|
|
||||||
let user = "race-user";
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
|
||||||
ip_tracker.set_user_limit(user, 100).await;
|
|
||||||
|
|
||||||
let iterations = 1000;
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
|
|
||||||
for i in 0..iterations {
|
|
||||||
let ip_tracker = Arc::clone(&ip_tracker);
|
|
||||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, (i % 254 + 1) as u8));
|
|
||||||
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
for _ in 0..10 {
|
|
||||||
if let Ok(()) = ip_tracker.check_and_add("race-user", ip).await {
|
|
||||||
ip_tracker.remove_ip("race-user", ip).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
futures::future::join_all(tasks).await;
|
|
||||||
|
|
||||||
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0, "IP count must be zero after balanced add/remove burst");
|
|
||||||
}
|
|
||||||
+196
-23
@@ -1,14 +1,14 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
@@ -17,14 +17,20 @@ use crate::crypto::SecureRandom;
|
|||||||
use crate::error::{ProxyError, Result};
|
use crate::error::{ProxyError, Result};
|
||||||
use crate::protocol::constants::*;
|
use crate::protocol::constants::*;
|
||||||
use crate::proxy::handshake::{HandshakeSuccess, encrypt_tg_nonce_with_ciphers, generate_tg_nonce};
|
use crate::proxy::handshake::{HandshakeSuccess, encrypt_tg_nonce_with_ciphers, generate_tg_nonce};
|
||||||
use crate::proxy::relay::relay_bidirectional;
|
|
||||||
use crate::proxy::route_mode::{
|
use crate::proxy::route_mode::{
|
||||||
RelayRouteMode, RouteCutoverState, ROUTE_SWITCH_ERROR_MSG, affected_cutover_state,
|
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
||||||
cutover_stagger_delay,
|
cutover_stagger_delay,
|
||||||
};
|
};
|
||||||
|
use crate::proxy::shared_state::{
|
||||||
|
ConntrackCloseEvent, ConntrackClosePublishResult, ConntrackCloseReason, ProxySharedState,
|
||||||
|
};
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use nix::fcntl::{Flock, FlockArg, OFlag, openat};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use nix::sys::stat::Mode;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
@@ -137,6 +143,7 @@ fn unknown_dc_log_path_is_still_safe(path: &SanitizedUnknownDcLogPath) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn open_unknown_dc_log_append(path: &Path) -> std::io::Result<std::fs::File> {
|
fn open_unknown_dc_log_append(path: &Path) -> std::io::Result<std::fs::File> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -156,6 +163,56 @@ fn open_unknown_dc_log_append(path: &Path) -> std::io::Result<std::fs::File> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_unknown_dc_log_append_anchored(
|
||||||
|
path: &SanitizedUnknownDcLogPath,
|
||||||
|
) -> std::io::Result<std::fs::File> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let parent = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
|
||||||
|
.open(&path.allowed_parent)?;
|
||||||
|
|
||||||
|
let oflags = OFlag::O_CREAT
|
||||||
|
| OFlag::O_APPEND
|
||||||
|
| OFlag::O_WRONLY
|
||||||
|
| OFlag::O_NOFOLLOW
|
||||||
|
| OFlag::O_CLOEXEC;
|
||||||
|
let mode = Mode::from_bits_truncate(0o600);
|
||||||
|
let path_component = Path::new(path.file_name.as_os_str());
|
||||||
|
let fd = openat(&parent, path_component, oflags, mode)
|
||||||
|
.map_err(|err| std::io::Error::from_raw_os_error(err as i32))?;
|
||||||
|
let file = std::fs::File::from(fd);
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
let _ = path;
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::PermissionDenied,
|
||||||
|
"unknown_dc_file_log_enabled requires unix O_NOFOLLOW support",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_unknown_dc_line(file: &mut std::fs::File, dc_idx: i16) -> std::io::Result<()> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let cloned = file.try_clone()?;
|
||||||
|
let mut locked = Flock::lock(cloned, FlockArg::LockExclusive)
|
||||||
|
.map_err(|(_, err)| std::io::Error::from_raw_os_error(err as i32))?;
|
||||||
|
let write_result = writeln!(&mut *locked, "dc_idx={dc_idx}");
|
||||||
|
let _ = locked
|
||||||
|
.unlock()
|
||||||
|
.map_err(|(_, err)| std::io::Error::from_raw_os_error(err as i32))?;
|
||||||
|
write_result
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
writeln!(file, "dc_idx={dc_idx}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn clear_unknown_dc_log_cache_for_testing() {
|
fn clear_unknown_dc_log_cache_for_testing() {
|
||||||
if let Some(set) = LOGGED_UNKNOWN_DCS.get()
|
if let Some(set) = LOGGED_UNKNOWN_DCS.get()
|
||||||
@@ -171,7 +228,43 @@ fn unknown_dc_test_lock() -> &'static Mutex<()> {
|
|||||||
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) async fn handle_via_direct<R, W>(
|
pub(crate) async fn handle_via_direct<R, W>(
|
||||||
|
client_reader: CryptoReader<R>,
|
||||||
|
client_writer: CryptoWriter<W>,
|
||||||
|
success: HandshakeSuccess,
|
||||||
|
upstream_manager: Arc<UpstreamManager>,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
config: Arc<ProxyConfig>,
|
||||||
|
buffer_pool: Arc<BufferPool>,
|
||||||
|
rng: Arc<SecureRandom>,
|
||||||
|
route_rx: watch::Receiver<RouteCutoverState>,
|
||||||
|
route_snapshot: RouteCutoverState,
|
||||||
|
session_id: u64,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
|
W: AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
handle_via_direct_with_shared(
|
||||||
|
client_reader,
|
||||||
|
client_writer,
|
||||||
|
success,
|
||||||
|
upstream_manager,
|
||||||
|
stats,
|
||||||
|
config.clone(),
|
||||||
|
buffer_pool,
|
||||||
|
rng,
|
||||||
|
route_rx,
|
||||||
|
route_snapshot,
|
||||||
|
session_id,
|
||||||
|
SocketAddr::from(([0, 0, 0, 0], config.server.port)),
|
||||||
|
ProxySharedState::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn handle_via_direct_with_shared<R, W>(
|
||||||
client_reader: CryptoReader<R>,
|
client_reader: CryptoReader<R>,
|
||||||
client_writer: CryptoWriter<W>,
|
client_writer: CryptoWriter<W>,
|
||||||
success: HandshakeSuccess,
|
success: HandshakeSuccess,
|
||||||
@@ -183,6 +276,8 @@ pub(crate) async fn handle_via_direct<R, W>(
|
|||||||
mut route_rx: watch::Receiver<RouteCutoverState>,
|
mut route_rx: watch::Receiver<RouteCutoverState>,
|
||||||
route_snapshot: RouteCutoverState,
|
route_snapshot: RouteCutoverState,
|
||||||
session_id: u64,
|
session_id: u64,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
) -> Result<()>
|
) -> Result<()>
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin + Send + 'static,
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
@@ -222,7 +317,19 @@ where
|
|||||||
stats.increment_user_connects(user);
|
stats.increment_user_connects(user);
|
||||||
let _direct_connection_lease = stats.acquire_direct_connection_lease();
|
let _direct_connection_lease = stats.acquire_direct_connection_lease();
|
||||||
|
|
||||||
let relay_result = relay_bidirectional(
|
let buffer_pool_trim = Arc::clone(&buffer_pool);
|
||||||
|
let relay_activity_timeout = if shared.conntrack_pressure_active() {
|
||||||
|
Duration::from_secs(
|
||||||
|
config
|
||||||
|
.server
|
||||||
|
.conntrack_control
|
||||||
|
.profile
|
||||||
|
.direct_activity_timeout_secs(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1800)
|
||||||
|
};
|
||||||
|
let relay_result = crate::proxy::relay::relay_bidirectional_with_activity_timeout(
|
||||||
client_reader,
|
client_reader,
|
||||||
client_writer,
|
client_writer,
|
||||||
tg_reader,
|
tg_reader,
|
||||||
@@ -233,14 +340,13 @@ where
|
|||||||
Arc::clone(&stats),
|
Arc::clone(&stats),
|
||||||
config.access.user_data_quota.get(user).copied(),
|
config.access.user_data_quota.get(user).copied(),
|
||||||
buffer_pool,
|
buffer_pool,
|
||||||
|
relay_activity_timeout,
|
||||||
);
|
);
|
||||||
tokio::pin!(relay_result);
|
tokio::pin!(relay_result);
|
||||||
let relay_result = loop {
|
let relay_result = loop {
|
||||||
if let Some(cutover) = affected_cutover_state(
|
if let Some(cutover) =
|
||||||
&route_rx,
|
affected_cutover_state(&route_rx, RelayRouteMode::Direct, route_snapshot.generation)
|
||||||
RelayRouteMode::Direct,
|
{
|
||||||
route_snapshot.generation,
|
|
||||||
) {
|
|
||||||
let delay = cutover_stagger_delay(session_id, cutover.generation);
|
let delay = cutover_stagger_delay(session_id, cutover.generation);
|
||||||
warn!(
|
warn!(
|
||||||
user = %user,
|
user = %user,
|
||||||
@@ -269,9 +375,59 @@ where
|
|||||||
Err(e) => debug!(user = %user, error = %e, "Direct relay ended with error"),
|
Err(e) => debug!(user = %user, error = %e, "Direct relay ended with error"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer_pool_trim.trim_to(buffer_pool_trim.max_buffers().min(64));
|
||||||
|
let pool_snapshot = buffer_pool_trim.stats();
|
||||||
|
stats.set_buffer_pool_gauges(
|
||||||
|
pool_snapshot.pooled,
|
||||||
|
pool_snapshot.allocated,
|
||||||
|
pool_snapshot.allocated.saturating_sub(pool_snapshot.pooled),
|
||||||
|
);
|
||||||
|
|
||||||
|
let close_reason = classify_conntrack_close_reason(&relay_result);
|
||||||
|
let publish_result = shared.publish_conntrack_close_event(ConntrackCloseEvent {
|
||||||
|
src: success.peer,
|
||||||
|
dst: local_addr,
|
||||||
|
reason: close_reason,
|
||||||
|
});
|
||||||
|
if !matches!(
|
||||||
|
publish_result,
|
||||||
|
ConntrackClosePublishResult::Sent | ConntrackClosePublishResult::Disabled
|
||||||
|
) {
|
||||||
|
stats.increment_conntrack_close_event_drop_total();
|
||||||
|
}
|
||||||
|
|
||||||
relay_result
|
relay_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn classify_conntrack_close_reason(result: &Result<()>) -> ConntrackCloseReason {
|
||||||
|
match result {
|
||||||
|
Ok(()) => ConntrackCloseReason::NormalEof,
|
||||||
|
Err(crate::error::ProxyError::Io(error))
|
||||||
|
if matches!(error.kind(), std::io::ErrorKind::TimedOut) =>
|
||||||
|
{
|
||||||
|
ConntrackCloseReason::Timeout
|
||||||
|
}
|
||||||
|
Err(crate::error::ProxyError::Io(error))
|
||||||
|
if matches!(
|
||||||
|
error.kind(),
|
||||||
|
std::io::ErrorKind::ConnectionReset
|
||||||
|
| std::io::ErrorKind::ConnectionAborted
|
||||||
|
| std::io::ErrorKind::BrokenPipe
|
||||||
|
| std::io::ErrorKind::NotConnected
|
||||||
|
| std::io::ErrorKind::UnexpectedEof
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
ConntrackCloseReason::Reset
|
||||||
|
}
|
||||||
|
Err(crate::error::ProxyError::Proxy(message))
|
||||||
|
if message.contains("pressure") || message.contains("evicted") =>
|
||||||
|
{
|
||||||
|
ConntrackCloseReason::Pressure
|
||||||
|
}
|
||||||
|
Err(_) => ConntrackCloseReason::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
|
fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
|
||||||
let prefer_v6 = config.network.prefer == 6 && config.network.ipv6.unwrap_or(true);
|
let prefer_v6 = config.network.prefer == 6 && config.network.ipv6.unwrap_or(true);
|
||||||
let datacenters = if prefer_v6 {
|
let datacenters = if prefer_v6 {
|
||||||
@@ -288,7 +444,9 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
|
|||||||
for addr_str in addrs {
|
for addr_str in addrs {
|
||||||
match addr_str.parse::<SocketAddr>() {
|
match addr_str.parse::<SocketAddr>() {
|
||||||
Ok(addr) => parsed.push(addr),
|
Ok(addr) => parsed.push(addr),
|
||||||
Err(_) => warn!(dc_idx = dc_idx, addr_str = %addr_str, "Invalid DC override address in config, ignoring"),
|
Err(_) => {
|
||||||
|
warn!(dc_idx = dc_idx, addr_str = %addr_str, "Invalid DC override address in config, ignoring")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +468,10 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
|
|||||||
|
|
||||||
// Unknown DC requested by client without override: log and fall back.
|
// Unknown DC requested by client without override: log and fall back.
|
||||||
if !config.dc_overrides.contains_key(&dc_key) {
|
if !config.dc_overrides.contains_key(&dc_key) {
|
||||||
warn!(dc_idx = dc_idx, "Requested non-standard DC with no override; falling back to default cluster");
|
warn!(
|
||||||
|
dc_idx = dc_idx,
|
||||||
|
"Requested non-standard DC with no override; falling back to default cluster"
|
||||||
|
);
|
||||||
if config.general.unknown_dc_file_log_enabled
|
if config.general.unknown_dc_file_log_enabled
|
||||||
&& let Some(path) = &config.general.unknown_dc_log_path
|
&& let Some(path) = &config.general.unknown_dc_log_path
|
||||||
&& let Ok(handle) = tokio::runtime::Handle::try_current()
|
&& let Ok(handle) = tokio::runtime::Handle::try_current()
|
||||||
@@ -319,9 +480,9 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
|
|||||||
if should_log_unknown_dc(dc_idx) {
|
if should_log_unknown_dc(dc_idx) {
|
||||||
handle.spawn_blocking(move || {
|
handle.spawn_blocking(move || {
|
||||||
if unknown_dc_log_path_is_still_safe(&path)
|
if unknown_dc_log_path_is_still_safe(&path)
|
||||||
&& let Ok(mut file) = open_unknown_dc_log_append(&path.resolved_path)
|
&& let Ok(mut file) = open_unknown_dc_log_append_anchored(&path)
|
||||||
{
|
{
|
||||||
let _ = writeln!(file, "dc_idx={dc_idx}");
|
let _ = append_unknown_dc_line(&mut file, dc_idx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -351,15 +512,15 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn do_tg_handshake_static(
|
async fn do_tg_handshake_static<S>(
|
||||||
mut stream: TcpStream,
|
mut stream: S,
|
||||||
success: &HandshakeSuccess,
|
success: &HandshakeSuccess,
|
||||||
config: &ProxyConfig,
|
config: &ProxyConfig,
|
||||||
rng: &SecureRandom,
|
rng: &SecureRandom,
|
||||||
) -> Result<(
|
) -> Result<(CryptoReader<ReadHalf<S>>, CryptoWriter<WriteHalf<S>>)>
|
||||||
CryptoReader<tokio::net::tcp::OwnedReadHalf>,
|
where
|
||||||
CryptoWriter<tokio::net::tcp::OwnedWriteHalf>,
|
S: AsyncRead + AsyncWrite + Unpin,
|
||||||
)> {
|
{
|
||||||
let (nonce, _tg_enc_key, _tg_enc_iv, _tg_dec_key, _tg_dec_iv) = generate_tg_nonce(
|
let (nonce, _tg_enc_key, _tg_enc_iv, _tg_dec_key, _tg_dec_iv) = generate_tg_nonce(
|
||||||
success.proto_tag,
|
success.proto_tag,
|
||||||
success.dc_idx,
|
success.dc_idx,
|
||||||
@@ -380,7 +541,7 @@ async fn do_tg_handshake_static(
|
|||||||
stream.write_all(&encrypted_nonce).await?;
|
stream.write_all(&encrypted_nonce).await?;
|
||||||
stream.flush().await?;
|
stream.flush().await?;
|
||||||
|
|
||||||
let (read_half, write_half) = stream.into_split();
|
let (read_half, write_half) = split(stream);
|
||||||
|
|
||||||
let max_pending = config.general.crypto_pending_buffer;
|
let max_pending = config.general.crypto_pending_buffer;
|
||||||
Ok((
|
Ok((
|
||||||
@@ -390,5 +551,17 @@ async fn do_tg_handshake_static(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "direct_relay_security_tests.rs"]
|
#[path = "tests/direct_relay_security_tests.rs"]
|
||||||
mod security_tests;
|
mod security_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/direct_relay_business_logic_tests.rs"]
|
||||||
|
mod business_logic_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/direct_relay_common_mistakes_tests.rs"]
|
||||||
|
mod common_mistakes_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/direct_relay_subtle_adversarial_tests.rs"]
|
||||||
|
mod subtle_adversarial_tests;
|
||||||
|
|||||||
+568
-238
File diff suppressed because it is too large
Load Diff
@@ -1,231 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use crate::crypto::sha256;
|
|
||||||
|
|
||||||
fn make_valid_mtproto_handshake(secret_hex: &str, proto_tag: ProtoTag, dc_idx: i16) -> [u8; HANDSHAKE_LEN] {
|
|
||||||
let secret = hex::decode(secret_hex).expect("secret hex must decode");
|
|
||||||
let mut handshake = [0x5Au8; HANDSHAKE_LEN];
|
|
||||||
for (idx, b) in handshake[SKIP_LEN..SKIP_LEN + PREKEY_LEN + IV_LEN]
|
|
||||||
.iter_mut()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
*b = (idx as u8).wrapping_add(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dec_prekey = &handshake[SKIP_LEN..SKIP_LEN + PREKEY_LEN];
|
|
||||||
let dec_iv_bytes = &handshake[SKIP_LEN + PREKEY_LEN..SKIP_LEN + PREKEY_LEN + IV_LEN];
|
|
||||||
|
|
||||||
let mut dec_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
|
|
||||||
dec_key_input.extend_from_slice(dec_prekey);
|
|
||||||
dec_key_input.extend_from_slice(&secret);
|
|
||||||
let dec_key = sha256(&dec_key_input);
|
|
||||||
|
|
||||||
let mut dec_iv_arr = [0u8; IV_LEN];
|
|
||||||
dec_iv_arr.copy_from_slice(dec_iv_bytes);
|
|
||||||
let dec_iv = u128::from_be_bytes(dec_iv_arr);
|
|
||||||
|
|
||||||
let mut stream = AesCtr::new(&dec_key, dec_iv);
|
|
||||||
let keystream = stream.encrypt(&[0u8; HANDSHAKE_LEN]);
|
|
||||||
|
|
||||||
let mut target_plain = [0u8; HANDSHAKE_LEN];
|
|
||||||
target_plain[PROTO_TAG_POS..PROTO_TAG_POS + 4].copy_from_slice(&proto_tag.to_bytes());
|
|
||||||
target_plain[DC_IDX_POS..DC_IDX_POS + 2].copy_from_slice(&dc_idx.to_le_bytes());
|
|
||||||
|
|
||||||
for idx in PROTO_TAG_POS..HANDSHAKE_LEN {
|
|
||||||
handshake[idx] = target_plain[idx] ^ keystream[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
handshake
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_probe_test_guard() -> std::sync::MutexGuard<'static, ()> {
|
|
||||||
auth_probe_test_lock()
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_config_with_secret_hex(secret_hex: &str) -> ProxyConfig {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.access.users.clear();
|
|
||||||
cfg.access.users.insert("user".to_string(), secret_hex.to_string());
|
|
||||||
cfg.access.ignore_time_skew = true;
|
|
||||||
cfg.general.modes.secure = true;
|
|
||||||
cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Mutational Bit-Flipping Tests (OWASP ASVS 5.1.4)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn mtproto_handshake_bit_flip_anywhere_rejected() {
|
|
||||||
let _guard = auth_probe_test_guard();
|
|
||||||
clear_auth_probe_state_for_testing();
|
|
||||||
|
|
||||||
let secret_hex = "11223344556677889900aabbccddeeff";
|
|
||||||
let base = make_valid_mtproto_handshake(secret_hex, ProtoTag::Secure, 2);
|
|
||||||
let config = test_config_with_secret_hex(secret_hex);
|
|
||||||
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
|
||||||
let peer: SocketAddr = "192.0.2.1:12345".parse().unwrap();
|
|
||||||
|
|
||||||
// Baseline check
|
|
||||||
let res = handle_mtproto_handshake(&base, tokio::io::empty(), tokio::io::sink(), peer, &config, &replay_checker, false, None).await;
|
|
||||||
match res {
|
|
||||||
HandshakeResult::Success(_) => {},
|
|
||||||
_ => panic!("Baseline failed: expected Success"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flip bits in the encrypted part (beyond the key material)
|
|
||||||
for byte_pos in SKIP_LEN..HANDSHAKE_LEN {
|
|
||||||
let mut h = base;
|
|
||||||
h[byte_pos] ^= 0x01; // Flip 1 bit
|
|
||||||
let res = handle_mtproto_handshake(&h, tokio::io::empty(), tokio::io::sink(), peer, &config, &replay_checker, false, None).await;
|
|
||||||
assert!(matches!(res, HandshakeResult::BadClient { .. }), "Flip at byte {byte_pos} bit 0 must be rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Adversarial Probing / Timing Neutrality (OWASP ASVS 5.1.7)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn mtproto_handshake_timing_neutrality_mocked() {
|
|
||||||
let secret_hex = "00112233445566778899aabbccddeeff";
|
|
||||||
let base = make_valid_mtproto_handshake(secret_hex, ProtoTag::Secure, 1);
|
|
||||||
let config = test_config_with_secret_hex(secret_hex);
|
|
||||||
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
|
||||||
let peer: SocketAddr = "192.0.2.2:54321".parse().unwrap();
|
|
||||||
|
|
||||||
const ITER: usize = 50;
|
|
||||||
|
|
||||||
let mut start = Instant::now();
|
|
||||||
for _ in 0..ITER {
|
|
||||||
let _ = handle_mtproto_handshake(&base, tokio::io::empty(), tokio::io::sink(), peer, &config, &replay_checker, false, None).await;
|
|
||||||
}
|
|
||||||
let duration_success = start.elapsed();
|
|
||||||
|
|
||||||
start = Instant::now();
|
|
||||||
for i in 0..ITER {
|
|
||||||
let mut h = base;
|
|
||||||
h[SKIP_LEN + (i % 48)] ^= 0xFF;
|
|
||||||
let _ = handle_mtproto_handshake(&h, tokio::io::empty(), tokio::io::sink(), peer, &config, &replay_checker, false, None).await;
|
|
||||||
}
|
|
||||||
let duration_fail = start.elapsed();
|
|
||||||
|
|
||||||
let avg_diff_ms = (duration_success.as_millis() as f64 - duration_fail.as_millis() as f64).abs() / ITER as f64;
|
|
||||||
|
|
||||||
// Threshold (loose for CI)
|
|
||||||
assert!(avg_diff_ms < 100.0, "Timing difference too large: {} ms/iter", avg_diff_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Stress Tests (OWASP ASVS 5.1.6)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn auth_probe_throttle_saturation_stress() {
|
|
||||||
let _guard = auth_probe_test_guard();
|
|
||||||
clear_auth_probe_state_for_testing();
|
|
||||||
|
|
||||||
let now = Instant::now();
|
|
||||||
|
|
||||||
// Record enough failures for one IP to trigger backoff
|
|
||||||
let target_ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
|
|
||||||
for _ in 0..AUTH_PROBE_BACKOFF_START_FAILS {
|
|
||||||
auth_probe_record_failure(target_ip, now);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(auth_probe_is_throttled(target_ip, now));
|
|
||||||
|
|
||||||
// Stress test with many unique IPs
|
|
||||||
for i in 0..500u32 {
|
|
||||||
let ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, (i % 256) as u8));
|
|
||||||
auth_probe_record_failure(ip, now);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tracked = AUTH_PROBE_STATE
|
|
||||||
.get()
|
|
||||||
.map(|state| state.len())
|
|
||||||
.unwrap_or(0);
|
|
||||||
assert!(
|
|
||||||
tracked <= AUTH_PROBE_TRACK_MAX_ENTRIES,
|
|
||||||
"auth probe state grew past hard cap: {tracked} > {AUTH_PROBE_TRACK_MAX_ENTRIES}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn mtproto_handshake_abridged_prefix_rejected() {
|
|
||||||
let _guard = auth_probe_test_guard();
|
|
||||||
clear_auth_probe_state_for_testing();
|
|
||||||
|
|
||||||
let mut handshake = [0x5Au8; HANDSHAKE_LEN];
|
|
||||||
handshake[0] = 0xef; // Abridged prefix
|
|
||||||
let config = ProxyConfig::default();
|
|
||||||
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
|
||||||
let peer: SocketAddr = "192.0.2.3:12345".parse().unwrap();
|
|
||||||
|
|
||||||
let res = handle_mtproto_handshake(&handshake, tokio::io::empty(), tokio::io::sink(), peer, &config, &replay_checker, false, None).await;
|
|
||||||
// MTProxy stops immediately on 0xef
|
|
||||||
assert!(matches!(res, HandshakeResult::BadClient { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn mtproto_handshake_preferred_user_mismatch_continues() {
|
|
||||||
let _guard = auth_probe_test_guard();
|
|
||||||
clear_auth_probe_state_for_testing();
|
|
||||||
|
|
||||||
let secret1_hex = "11111111111111111111111111111111";
|
|
||||||
let secret2_hex = "22222222222222222222222222222222";
|
|
||||||
|
|
||||||
let base = make_valid_mtproto_handshake(secret2_hex, ProtoTag::Secure, 1);
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.access.users.insert("user1".to_string(), secret1_hex.to_string());
|
|
||||||
config.access.users.insert("user2".to_string(), secret2_hex.to_string());
|
|
||||||
config.access.ignore_time_skew = true;
|
|
||||||
config.general.modes.secure = true;
|
|
||||||
|
|
||||||
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
|
||||||
let peer: SocketAddr = "192.0.2.4:12345".parse().unwrap();
|
|
||||||
|
|
||||||
// Even if we prefer user1, if user2 matches, it should succeed.
|
|
||||||
let res = handle_mtproto_handshake(&base, tokio::io::empty(), tokio::io::sink(), peer, &config, &replay_checker, false, Some("user1")).await;
|
|
||||||
if let HandshakeResult::Success((_, _, success)) = res {
|
|
||||||
assert_eq!(success.user, "user2");
|
|
||||||
} else {
|
|
||||||
panic!("Handshake failed even though user2 matched");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn mtproto_handshake_concurrent_flood_stability() {
|
|
||||||
let _guard = auth_probe_test_guard();
|
|
||||||
clear_auth_probe_state_for_testing();
|
|
||||||
|
|
||||||
let secret_hex = "00112233445566778899aabbccddeeff";
|
|
||||||
let base = make_valid_mtproto_handshake(secret_hex, ProtoTag::Secure, 1);
|
|
||||||
let mut config = test_config_with_secret_hex(secret_hex);
|
|
||||||
config.access.ignore_time_skew = true;
|
|
||||||
let replay_checker = Arc::new(ReplayChecker::new(1024, Duration::from_secs(60)));
|
|
||||||
let config = Arc::new(config);
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
for i in 0..50 {
|
|
||||||
let base = base;
|
|
||||||
let config = Arc::clone(&config);
|
|
||||||
let replay_checker = Arc::clone(&replay_checker);
|
|
||||||
let peer: SocketAddr = format!("192.0.2.{}:12345", (i % 254) + 1).parse().unwrap();
|
|
||||||
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
let res = handle_mtproto_handshake(&base, tokio::io::empty(), tokio::io::sink(), peer, &config, &replay_checker, false, None).await;
|
|
||||||
matches!(res, HandshakeResult::Success(_))
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't necessarily care if they all succeed (some might fail due to replay if they hit the same chunk),
|
|
||||||
// but the system must not panic or hang.
|
|
||||||
for task in tasks {
|
|
||||||
let _ = task.await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+831
-100
File diff suppressed because it is too large
Load Diff
@@ -1,213 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::io::duplex;
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use tokio::time::{Instant, Duration};
|
|
||||||
use crate::config::ProxyConfig;
|
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Probing Indistinguishability (OWASP ASVS 5.1.7)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn masking_probes_indistinguishable_timing() {
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.censorship.mask = true;
|
|
||||||
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
config.censorship.mask_port = 80; // Should timeout/refuse
|
|
||||||
|
|
||||||
let peer: SocketAddr = "192.0.2.10:443".parse().unwrap();
|
|
||||||
let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
||||||
let beobachten = BeobachtenStore::new();
|
|
||||||
|
|
||||||
// Test different probe types
|
|
||||||
let probes = vec![
|
|
||||||
(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n".to_vec(), "HTTP"),
|
|
||||||
(b"SSH-2.0-probe".to_vec(), "SSH"),
|
|
||||||
(vec![0x16, 0x03, 0x03, 0x00, 0x05, 0x01, 0x00, 0x00, 0x01, 0x00], "TLS-scanner"),
|
|
||||||
(vec![0x42; 5], "port-scanner"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (probe, type_name) in probes {
|
|
||||||
let (client_reader, _client_writer) = duplex(256);
|
|
||||||
let (_client_visible_reader, client_visible_writer) = duplex(256);
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
handle_bad_client(
|
|
||||||
client_reader,
|
|
||||||
client_visible_writer,
|
|
||||||
&probe,
|
|
||||||
peer,
|
|
||||||
local_addr,
|
|
||||||
&config,
|
|
||||||
&beobachten,
|
|
||||||
).await;
|
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
|
|
||||||
// We expect any outcome to take roughly MASK_TIMEOUT (50ms in tests)
|
|
||||||
// to mask whether the backend was reachable or refused.
|
|
||||||
assert!(elapsed >= Duration::from_millis(30), "Probe {type_name} finished too fast: {elapsed:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Masking Budget Stress Tests (OWASP ASVS 5.1.6)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn masking_budget_stress_under_load() {
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.censorship.mask = true;
|
|
||||||
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
config.censorship.mask_port = 1; // Unlikely port
|
|
||||||
|
|
||||||
let peer: SocketAddr = "192.0.2.20:443".parse().unwrap();
|
|
||||||
let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
for _ in 0..50 {
|
|
||||||
let (client_reader, _client_writer) = duplex(256);
|
|
||||||
let (_client_visible_reader, client_visible_writer) = duplex(256);
|
|
||||||
let config = config.clone();
|
|
||||||
let beobachten = Arc::clone(&beobachten);
|
|
||||||
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
let start = Instant::now();
|
|
||||||
handle_bad_client(
|
|
||||||
client_reader,
|
|
||||||
client_visible_writer,
|
|
||||||
b"probe",
|
|
||||||
peer,
|
|
||||||
local_addr,
|
|
||||||
&config,
|
|
||||||
&beobachten,
|
|
||||||
).await;
|
|
||||||
start.elapsed()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
for task in tasks {
|
|
||||||
let elapsed = task.await.unwrap();
|
|
||||||
assert!(elapsed >= Duration::from_millis(30), "Stress probe finished too fast: {elapsed:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// detect_client_type Fingerprint Check
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_detect_client_type_boundary_cases() {
|
|
||||||
// 9 bytes = port-scanner
|
|
||||||
assert_eq!(detect_client_type(&[0x42; 9]), "port-scanner");
|
|
||||||
// 10 bytes = unknown
|
|
||||||
assert_eq!(detect_client_type(&[0x42; 10]), "unknown");
|
|
||||||
|
|
||||||
// HTTP verbs without trailing space
|
|
||||||
assert_eq!(detect_client_type(b"GET/"), "port-scanner"); // because len < 10
|
|
||||||
assert_eq!(detect_client_type(b"GET /path"), "HTTP");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Priority 2: Slowloris and Slow Read Attacks (OWASP ASVS 5.1.5)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn masking_slowloris_client_idle_timeout_rejected() {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
||||||
let backend_addr = listener.local_addr().unwrap();
|
|
||||||
let initial = b"GET / HTTP/1.1\r\nHost: front.example\r\n\r\n".to_vec();
|
|
||||||
|
|
||||||
let accept_task = tokio::spawn({
|
|
||||||
let initial = initial.clone();
|
|
||||||
async move {
|
|
||||||
let (mut stream, _) = listener.accept().await.unwrap();
|
|
||||||
let mut observed = vec![0u8; initial.len()];
|
|
||||||
stream.read_exact(&mut observed).await.unwrap();
|
|
||||||
assert_eq!(observed, initial);
|
|
||||||
|
|
||||||
let mut drip = [0u8; 1];
|
|
||||||
let drip_read = tokio::time::timeout(Duration::from_millis(220), stream.read_exact(&mut drip)).await;
|
|
||||||
assert!(
|
|
||||||
drip_read.is_err() || drip_read.unwrap().is_err(),
|
|
||||||
"backend must not receive post-timeout slowloris drip bytes"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.censorship.mask = true;
|
|
||||||
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
config.censorship.mask_port = backend_addr.port();
|
|
||||||
|
|
||||||
let beobachten = BeobachtenStore::new();
|
|
||||||
let peer: SocketAddr = "192.0.2.10:12345".parse().unwrap();
|
|
||||||
let local: SocketAddr = "192.0.2.1:443".parse().unwrap();
|
|
||||||
|
|
||||||
let (mut client_writer, client_reader) = duplex(1024);
|
|
||||||
let (_client_visible_reader, client_visible_writer) = duplex(1024);
|
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
|
||||||
handle_bad_client(
|
|
||||||
client_reader,
|
|
||||||
client_visible_writer,
|
|
||||||
&initial,
|
|
||||||
peer,
|
|
||||||
local,
|
|
||||||
&config,
|
|
||||||
&beobachten,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_millis(160)).await;
|
|
||||||
let _ = client_writer.write_all(b"X").await;
|
|
||||||
|
|
||||||
handle.await.unwrap();
|
|
||||||
accept_task.await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Priority 2: Fallback Server Down / Fingerprinting (OWASP ASVS 5.1.7)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn masking_fallback_down_mimics_timeout() {
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.censorship.mask = true;
|
|
||||||
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
config.censorship.mask_port = 1; // Unlikely port
|
|
||||||
|
|
||||||
let (server_reader, server_writer) = duplex(1024);
|
|
||||||
let beobachten = BeobachtenStore::new();
|
|
||||||
let peer: SocketAddr = "192.0.2.12:12345".parse().unwrap();
|
|
||||||
let local: SocketAddr = "192.0.2.1:443".parse().unwrap();
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
handle_bad_client(server_reader, server_writer, b"GET / HTTP/1.1\r\n", peer, local, &config, &beobachten).await;
|
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
// It should wait for MASK_TIMEOUT (50ms in tests) even if connection was refused immediately
|
|
||||||
assert!(elapsed >= Duration::from_millis(40), "Must respect connect budget even on failure: {:?}", elapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Priority 2: SSRF Prevention (OWASP ASVS 5.1.2)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn masking_ssrf_resolve_internal_ranges_blocked() {
|
|
||||||
use crate::network::dns_overrides::resolve_socket_addr;
|
|
||||||
|
|
||||||
let blocked_ips = ["127.0.0.1", "169.254.169.254", "10.0.0.1", "192.168.1.1", "0.0.0.0"];
|
|
||||||
|
|
||||||
for ip in blocked_ips {
|
|
||||||
assert!(
|
|
||||||
resolve_socket_addr(ip, 80).is_none(),
|
|
||||||
"runtime DNS overrides must not resolve unconfigured literal host targets"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1578
-282
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+74
-1
@@ -1,12 +1,73 @@
|
|||||||
//! Proxy Defs
|
//! Proxy Defs
|
||||||
|
|
||||||
|
// Apply strict linting to proxy production code while keeping test builds noise-tolerant.
|
||||||
|
#![cfg_attr(test, allow(warnings))]
|
||||||
|
#![cfg_attr(not(test), forbid(clippy::undocumented_unsafe_blocks))]
|
||||||
|
#![cfg_attr(
|
||||||
|
not(test),
|
||||||
|
deny(
|
||||||
|
clippy::unwrap_used,
|
||||||
|
clippy::expect_used,
|
||||||
|
clippy::panic,
|
||||||
|
clippy::todo,
|
||||||
|
clippy::unimplemented,
|
||||||
|
clippy::correctness,
|
||||||
|
clippy::option_if_let_else,
|
||||||
|
clippy::or_fun_call,
|
||||||
|
clippy::branches_sharing_code,
|
||||||
|
clippy::single_option_map,
|
||||||
|
clippy::useless_let_if_seq,
|
||||||
|
clippy::redundant_locals,
|
||||||
|
clippy::cloned_ref_to_slice_refs,
|
||||||
|
unsafe_code,
|
||||||
|
clippy::await_holding_lock,
|
||||||
|
clippy::await_holding_refcell_ref,
|
||||||
|
clippy::debug_assert_with_mut_call,
|
||||||
|
clippy::macro_use_imports,
|
||||||
|
clippy::cast_ptr_alignment,
|
||||||
|
clippy::cast_lossless,
|
||||||
|
clippy::ptr_as_ptr,
|
||||||
|
clippy::large_stack_arrays,
|
||||||
|
clippy::same_functions_in_if_condition,
|
||||||
|
trivial_casts,
|
||||||
|
trivial_numeric_casts,
|
||||||
|
unused_extern_crates,
|
||||||
|
unused_import_braces,
|
||||||
|
rust_2018_idioms
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#![cfg_attr(
|
||||||
|
not(test),
|
||||||
|
allow(
|
||||||
|
clippy::use_self,
|
||||||
|
clippy::redundant_closure,
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::doc_markdown,
|
||||||
|
clippy::missing_const_for_fn,
|
||||||
|
clippy::unnecessary_operation,
|
||||||
|
clippy::redundant_pub_crate,
|
||||||
|
clippy::derive_partial_eq_without_eq,
|
||||||
|
clippy::type_complexity,
|
||||||
|
clippy::new_ret_no_self,
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_possible_wrap,
|
||||||
|
clippy::significant_drop_tightening,
|
||||||
|
clippy::significant_drop_in_scrutinee,
|
||||||
|
clippy::float_cmp,
|
||||||
|
clippy::nursery
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
|
||||||
|
pub mod adaptive_buffers;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod direct_relay;
|
pub mod direct_relay;
|
||||||
pub mod handshake;
|
pub mod handshake;
|
||||||
pub mod masking;
|
pub mod masking;
|
||||||
pub mod middle_relay;
|
pub mod middle_relay;
|
||||||
pub mod route_mode;
|
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
|
pub mod route_mode;
|
||||||
|
pub mod session_eviction;
|
||||||
|
pub mod shared_state;
|
||||||
|
|
||||||
pub use client::ClientHandler;
|
pub use client::ClientHandler;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
@@ -15,3 +76,15 @@ pub use handshake::*;
|
|||||||
pub use masking::*;
|
pub use masking::*;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use relay::*;
|
pub use relay::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/test_harness_common.rs"]
|
||||||
|
mod test_harness_common;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/proxy_shared_state_isolation_tests.rs"]
|
||||||
|
mod proxy_shared_state_isolation_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/proxy_shared_state_parallel_execution_tests.rs"]
|
||||||
|
mod proxy_shared_state_parallel_execution_tests;
|
||||||
|
|||||||
+176
-124
@@ -51,21 +51,18 @@
|
|||||||
//! - `poll_write` on client = S→C (to client) → `octets_to`, `msgs_to`
|
//! - `poll_write` on client = S→C (to client) → `octets_to`, `msgs_to`
|
||||||
//! - `SharedCounters` (atomics) let the watchdog read stats without locking
|
//! - `SharedCounters` (atomics) let the watchdog read stats without locking
|
||||||
|
|
||||||
|
use crate::error::{ProxyError, Result};
|
||||||
|
use crate::stats::{Stats, UserStats};
|
||||||
|
use crate::stream::BufferPool;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use dashmap::DashMap;
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf, copy_bidirectional_with_sizes};
|
||||||
use tokio::io::{
|
|
||||||
AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf, copy_bidirectional_with_sizes,
|
|
||||||
};
|
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tracing::{debug, trace, warn};
|
use tracing::{debug, trace, warn};
|
||||||
use crate::error::{ProxyError, Result};
|
|
||||||
use crate::stats::Stats;
|
|
||||||
use crate::stream::BufferPool;
|
|
||||||
|
|
||||||
// ============= Constants =============
|
// ============= Constants =============
|
||||||
|
|
||||||
@@ -73,6 +70,7 @@ use crate::stream::BufferPool;
|
|||||||
///
|
///
|
||||||
/// iOS keeps Telegram connections alive in background for up to 30 minutes.
|
/// iOS keeps Telegram connections alive in background for up to 30 minutes.
|
||||||
/// Closing earlier causes unnecessary reconnects and handshake overhead.
|
/// Closing earlier causes unnecessary reconnects and handshake overhead.
|
||||||
|
#[allow(dead_code)]
|
||||||
const ACTIVITY_TIMEOUT: Duration = Duration::from_secs(1800);
|
const ACTIVITY_TIMEOUT: Duration = Duration::from_secs(1800);
|
||||||
|
|
||||||
/// Watchdog check interval — also used for periodic rate logging.
|
/// Watchdog check interval — also used for periodic rate logging.
|
||||||
@@ -81,6 +79,11 @@ const ACTIVITY_TIMEOUT: Duration = Duration::from_secs(1800);
|
|||||||
/// without measurable overhead from atomic reads.
|
/// without measurable overhead from atomic reads.
|
||||||
const WATCHDOG_INTERVAL: Duration = Duration::from_secs(10);
|
const WATCHDOG_INTERVAL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn watchdog_delta(current: u64, previous: u64) -> u64 {
|
||||||
|
current.saturating_sub(previous)
|
||||||
|
}
|
||||||
|
|
||||||
// ============= CombinedStream =============
|
// ============= CombinedStream =============
|
||||||
|
|
||||||
/// Combines separate read and write halves into a single bidirectional stream.
|
/// Combines separate read and write halves into a single bidirectional stream.
|
||||||
@@ -206,10 +209,10 @@ struct StatsIo<S> {
|
|||||||
counters: Arc<SharedCounters>,
|
counters: Arc<SharedCounters>,
|
||||||
stats: Arc<Stats>,
|
stats: Arc<Stats>,
|
||||||
user: String,
|
user: String,
|
||||||
|
user_stats: Arc<UserStats>,
|
||||||
quota_limit: Option<u64>,
|
quota_limit: Option<u64>,
|
||||||
quota_exceeded: Arc<AtomicBool>,
|
quota_exceeded: Arc<AtomicBool>,
|
||||||
quota_read_wake_scheduled: bool,
|
quota_bytes_since_check: u64,
|
||||||
quota_write_wake_scheduled: bool,
|
|
||||||
epoch: Instant,
|
epoch: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,15 +228,16 @@ impl<S> StatsIo<S> {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
// Mark initial activity so the watchdog doesn't fire before data flows
|
// Mark initial activity so the watchdog doesn't fire before data flows
|
||||||
counters.touch(Instant::now(), epoch);
|
counters.touch(Instant::now(), epoch);
|
||||||
|
let user_stats = stats.get_or_create_user_stats_handle(&user);
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
counters,
|
counters,
|
||||||
stats,
|
stats,
|
||||||
user,
|
user,
|
||||||
|
user_stats,
|
||||||
quota_limit,
|
quota_limit,
|
||||||
quota_exceeded,
|
quota_exceeded,
|
||||||
quota_read_wake_scheduled: false,
|
quota_bytes_since_check: 0,
|
||||||
quota_write_wake_scheduled: false,
|
|
||||||
epoch,
|
epoch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,22 +266,22 @@ fn is_quota_io_error(err: &io::Error) -> bool {
|
|||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
static QUOTA_USER_LOCKS: OnceLock<DashMap<String, Arc<Mutex<()>>>> = OnceLock::new();
|
const QUOTA_NEAR_LIMIT_BYTES: u64 = 64 * 1024;
|
||||||
|
const QUOTA_LARGE_CHARGE_BYTES: u64 = 16 * 1024;
|
||||||
|
const QUOTA_ADAPTIVE_INTERVAL_MIN_BYTES: u64 = 4 * 1024;
|
||||||
|
const QUOTA_ADAPTIVE_INTERVAL_MAX_BYTES: u64 = 64 * 1024;
|
||||||
|
|
||||||
fn quota_user_lock(user: &str) -> Arc<Mutex<()>> {
|
#[inline]
|
||||||
let locks = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
fn quota_adaptive_interval_bytes(remaining_before: u64) -> u64 {
|
||||||
if let Some(existing) = locks.get(user) {
|
remaining_before.saturating_div(2).clamp(
|
||||||
return Arc::clone(existing.value());
|
QUOTA_ADAPTIVE_INTERVAL_MIN_BYTES,
|
||||||
}
|
QUOTA_ADAPTIVE_INTERVAL_MAX_BYTES,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let created = Arc::new(Mutex::new(()));
|
#[inline]
|
||||||
match locks.entry(user.to_string()) {
|
fn should_immediate_quota_check(remaining_before: u64, charge_bytes: u64) -> bool {
|
||||||
dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()),
|
remaining_before <= QUOTA_NEAR_LIMIT_BYTES || charge_bytes >= QUOTA_LARGE_CHARGE_BYTES
|
||||||
dashmap::mapref::entry::Entry::Vacant(entry) => {
|
|
||||||
entry.insert(Arc::clone(&created));
|
|
||||||
created
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
||||||
@@ -287,61 +291,60 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
|||||||
buf: &mut ReadBuf<'_>,
|
buf: &mut ReadBuf<'_>,
|
||||||
) -> Poll<io::Result<()>> {
|
) -> Poll<io::Result<()>> {
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
if this.quota_exceeded.load(Ordering::Relaxed) {
|
if this.quota_exceeded.load(Ordering::Acquire) {
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let quota_lock = this
|
let mut remaining_before = None;
|
||||||
.quota_limit
|
if let Some(limit) = this.quota_limit {
|
||||||
.is_some()
|
let used_before = this.user_stats.quota_used();
|
||||||
.then(|| quota_user_lock(&this.user));
|
let remaining = limit.saturating_sub(used_before);
|
||||||
let _quota_guard = if let Some(lock) = quota_lock.as_ref() {
|
if remaining == 0 {
|
||||||
match lock.try_lock() {
|
this.quota_exceeded.store(true, Ordering::Release);
|
||||||
Ok(guard) => {
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
this.quota_read_wake_scheduled = false;
|
|
||||||
Some(guard)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
if !this.quota_read_wake_scheduled {
|
|
||||||
this.quota_read_wake_scheduled = true;
|
|
||||||
let waker = cx.waker().clone();
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
waker.wake();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
remaining_before = Some(remaining);
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(limit) = this.quota_limit
|
|
||||||
&& this.stats.get_user_total_octets(&this.user) >= limit
|
|
||||||
{
|
|
||||||
this.quota_exceeded.store(true, Ordering::Relaxed);
|
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let before = buf.filled().len();
|
let before = buf.filled().len();
|
||||||
|
|
||||||
match Pin::new(&mut this.inner).poll_read(cx, buf) {
|
match Pin::new(&mut this.inner).poll_read(cx, buf) {
|
||||||
Poll::Ready(Ok(())) => {
|
Poll::Ready(Ok(())) => {
|
||||||
let n = buf.filled().len() - before;
|
let n = buf.filled().len() - before;
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
|
let n_to_charge = n as u64;
|
||||||
|
|
||||||
// C→S: client sent data
|
// C→S: client sent data
|
||||||
this.counters.c2s_bytes.fetch_add(n as u64, Ordering::Relaxed);
|
this.counters
|
||||||
|
.c2s_bytes
|
||||||
|
.fetch_add(n_to_charge, Ordering::Relaxed);
|
||||||
this.counters.c2s_ops.fetch_add(1, Ordering::Relaxed);
|
this.counters.c2s_ops.fetch_add(1, Ordering::Relaxed);
|
||||||
this.counters.touch(Instant::now(), this.epoch);
|
this.counters.touch(Instant::now(), this.epoch);
|
||||||
|
|
||||||
this.stats.add_user_octets_from(&this.user, n as u64);
|
this.stats
|
||||||
this.stats.increment_user_msgs_from(&this.user);
|
.add_user_octets_from_handle(this.user_stats.as_ref(), n_to_charge);
|
||||||
|
this.stats
|
||||||
|
.increment_user_msgs_from_handle(this.user_stats.as_ref());
|
||||||
|
|
||||||
if let Some(limit) = this.quota_limit
|
if let (Some(limit), Some(remaining)) = (this.quota_limit, remaining_before) {
|
||||||
&& this.stats.get_user_total_octets(&this.user) >= limit
|
this.stats
|
||||||
{
|
.quota_charge_post_write(this.user_stats.as_ref(), n_to_charge);
|
||||||
this.quota_exceeded.store(true, Ordering::Relaxed);
|
if should_immediate_quota_check(remaining, n_to_charge) {
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
this.quota_bytes_since_check = 0;
|
||||||
|
if this.user_stats.quota_used() >= limit {
|
||||||
|
this.quota_exceeded.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.quota_bytes_since_check =
|
||||||
|
this.quota_bytes_since_check.saturating_add(n_to_charge);
|
||||||
|
let interval = quota_adaptive_interval_bytes(remaining);
|
||||||
|
if this.quota_bytes_since_check >= interval {
|
||||||
|
this.quota_bytes_since_check = 0;
|
||||||
|
if this.user_stats.quota_used() >= limit {
|
||||||
|
this.quota_exceeded.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(user = %this.user, bytes = n, "C->S");
|
trace!(user = %this.user, bytes = n, "C->S");
|
||||||
@@ -360,71 +363,57 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
|||||||
buf: &[u8],
|
buf: &[u8],
|
||||||
) -> Poll<io::Result<usize>> {
|
) -> Poll<io::Result<usize>> {
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
if this.quota_exceeded.load(Ordering::Relaxed) {
|
if this.quota_exceeded.load(Ordering::Acquire) {
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let quota_lock = this
|
let mut remaining_before = None;
|
||||||
.quota_limit
|
if let Some(limit) = this.quota_limit {
|
||||||
.is_some()
|
let used_before = this.user_stats.quota_used();
|
||||||
.then(|| quota_user_lock(&this.user));
|
let remaining = limit.saturating_sub(used_before);
|
||||||
let _quota_guard = if let Some(lock) = quota_lock.as_ref() {
|
if remaining == 0 {
|
||||||
match lock.try_lock() {
|
this.quota_exceeded.store(true, Ordering::Release);
|
||||||
Ok(guard) => {
|
|
||||||
this.quota_write_wake_scheduled = false;
|
|
||||||
Some(guard)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
if !this.quota_write_wake_scheduled {
|
|
||||||
this.quota_write_wake_scheduled = true;
|
|
||||||
let waker = cx.waker().clone();
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
waker.wake();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let write_buf = if let Some(limit) = this.quota_limit {
|
|
||||||
let used = this.stats.get_user_total_octets(&this.user);
|
|
||||||
if used >= limit {
|
|
||||||
this.quota_exceeded.store(true, Ordering::Relaxed);
|
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
}
|
}
|
||||||
|
remaining_before = Some(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
let remaining = (limit - used) as usize;
|
match Pin::new(&mut this.inner).poll_write(cx, buf) {
|
||||||
if buf.len() > remaining {
|
|
||||||
// Fail closed: do not emit partial S->C payload when remaining
|
|
||||||
// quota cannot accommodate the pending write request.
|
|
||||||
this.quota_exceeded.store(true, Ordering::Relaxed);
|
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
|
||||||
}
|
|
||||||
buf
|
|
||||||
} else {
|
|
||||||
buf
|
|
||||||
};
|
|
||||||
|
|
||||||
match Pin::new(&mut this.inner).poll_write(cx, write_buf) {
|
|
||||||
Poll::Ready(Ok(n)) => {
|
Poll::Ready(Ok(n)) => {
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
|
let n_to_charge = n as u64;
|
||||||
|
|
||||||
// S→C: data written to client
|
// S→C: data written to client
|
||||||
this.counters.s2c_bytes.fetch_add(n as u64, Ordering::Relaxed);
|
this.counters
|
||||||
|
.s2c_bytes
|
||||||
|
.fetch_add(n_to_charge, Ordering::Relaxed);
|
||||||
this.counters.s2c_ops.fetch_add(1, Ordering::Relaxed);
|
this.counters.s2c_ops.fetch_add(1, Ordering::Relaxed);
|
||||||
this.counters.touch(Instant::now(), this.epoch);
|
this.counters.touch(Instant::now(), this.epoch);
|
||||||
|
|
||||||
this.stats.add_user_octets_to(&this.user, n as u64);
|
this.stats
|
||||||
this.stats.increment_user_msgs_to(&this.user);
|
.add_user_octets_to_handle(this.user_stats.as_ref(), n_to_charge);
|
||||||
|
this.stats
|
||||||
|
.increment_user_msgs_to_handle(this.user_stats.as_ref());
|
||||||
|
|
||||||
if let Some(limit) = this.quota_limit
|
if let (Some(limit), Some(remaining)) = (this.quota_limit, remaining_before) {
|
||||||
&& this.stats.get_user_total_octets(&this.user) >= limit
|
this.stats
|
||||||
{
|
.quota_charge_post_write(this.user_stats.as_ref(), n_to_charge);
|
||||||
this.quota_exceeded.store(true, Ordering::Relaxed);
|
if should_immediate_quota_check(remaining, n_to_charge) {
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
this.quota_bytes_since_check = 0;
|
||||||
|
if this.user_stats.quota_used() >= limit {
|
||||||
|
this.quota_exceeded.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.quota_bytes_since_check =
|
||||||
|
this.quota_bytes_since_check.saturating_add(n_to_charge);
|
||||||
|
let interval = quota_adaptive_interval_bytes(remaining);
|
||||||
|
if this.quota_bytes_since_check >= interval {
|
||||||
|
this.quota_bytes_since_check = 0;
|
||||||
|
if this.user_stats.quota_used() >= limit {
|
||||||
|
this.quota_exceeded.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(user = %this.user, bytes = n, "S->C");
|
trace!(user = %this.user, bytes = n, "S->C");
|
||||||
@@ -465,6 +454,7 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
|||||||
/// - Clean shutdown: both write sides are shut down on exit
|
/// - Clean shutdown: both write sides are shut down on exit
|
||||||
/// - Error propagation: quota exits return `ProxyError::DataQuotaExceeded`,
|
/// - Error propagation: quota exits return `ProxyError::DataQuotaExceeded`,
|
||||||
/// other I/O failures are returned as `ProxyError::Io`
|
/// other I/O failures are returned as `ProxyError::Io`
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn relay_bidirectional<CR, CW, SR, SW>(
|
pub async fn relay_bidirectional<CR, CW, SR, SW>(
|
||||||
client_reader: CR,
|
client_reader: CR,
|
||||||
client_writer: CW,
|
client_writer: CW,
|
||||||
@@ -483,6 +473,42 @@ where
|
|||||||
SR: AsyncRead + Unpin + Send + 'static,
|
SR: AsyncRead + Unpin + Send + 'static,
|
||||||
SW: AsyncWrite + Unpin + Send + 'static,
|
SW: AsyncWrite + Unpin + Send + 'static,
|
||||||
{
|
{
|
||||||
|
relay_bidirectional_with_activity_timeout(
|
||||||
|
client_reader,
|
||||||
|
client_writer,
|
||||||
|
server_reader,
|
||||||
|
server_writer,
|
||||||
|
c2s_buf_size,
|
||||||
|
s2c_buf_size,
|
||||||
|
user,
|
||||||
|
stats,
|
||||||
|
quota_limit,
|
||||||
|
_buffer_pool,
|
||||||
|
ACTIVITY_TIMEOUT,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn relay_bidirectional_with_activity_timeout<CR, CW, SR, SW>(
|
||||||
|
client_reader: CR,
|
||||||
|
client_writer: CW,
|
||||||
|
server_reader: SR,
|
||||||
|
server_writer: SW,
|
||||||
|
c2s_buf_size: usize,
|
||||||
|
s2c_buf_size: usize,
|
||||||
|
user: &str,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
quota_limit: Option<u64>,
|
||||||
|
_buffer_pool: Arc<BufferPool>,
|
||||||
|
activity_timeout: Duration,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
CR: AsyncRead + Unpin + Send + 'static,
|
||||||
|
CW: AsyncWrite + Unpin + Send + 'static,
|
||||||
|
SR: AsyncRead + Unpin + Send + 'static,
|
||||||
|
SW: AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
let activity_timeout = activity_timeout.max(Duration::from_secs(1));
|
||||||
let epoch = Instant::now();
|
let epoch = Instant::now();
|
||||||
let counters = Arc::new(SharedCounters::new());
|
let counters = Arc::new(SharedCounters::new());
|
||||||
let quota_exceeded = Arc::new(AtomicBool::new(false));
|
let quota_exceeded = Arc::new(AtomicBool::new(false));
|
||||||
@@ -518,13 +544,13 @@ where
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let idle = wd_counters.idle_duration(now, epoch);
|
let idle = wd_counters.idle_duration(now, epoch);
|
||||||
|
|
||||||
if wd_quota_exceeded.load(Ordering::Relaxed) {
|
if wd_quota_exceeded.load(Ordering::Acquire) {
|
||||||
warn!(user = %wd_user, "User data quota reached, closing relay");
|
warn!(user = %wd_user, "User data quota reached, closing relay");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Activity timeout ────────────────────────────────────
|
// ── Activity timeout ────────────────────────────────────
|
||||||
if idle >= ACTIVITY_TIMEOUT {
|
if idle >= activity_timeout {
|
||||||
let c2s = wd_counters.c2s_bytes.load(Ordering::Relaxed);
|
let c2s = wd_counters.c2s_bytes.load(Ordering::Relaxed);
|
||||||
let s2c = wd_counters.s2c_bytes.load(Ordering::Relaxed);
|
let s2c = wd_counters.s2c_bytes.load(Ordering::Relaxed);
|
||||||
warn!(
|
warn!(
|
||||||
@@ -540,8 +566,8 @@ where
|
|||||||
// ── Periodic rate logging ───────────────────────────────
|
// ── Periodic rate logging ───────────────────────────────
|
||||||
let c2s = wd_counters.c2s_bytes.load(Ordering::Relaxed);
|
let c2s = wd_counters.c2s_bytes.load(Ordering::Relaxed);
|
||||||
let s2c = wd_counters.s2c_bytes.load(Ordering::Relaxed);
|
let s2c = wd_counters.s2c_bytes.load(Ordering::Relaxed);
|
||||||
let c2s_delta = c2s - prev_c2s;
|
let c2s_delta = watchdog_delta(c2s, prev_c2s);
|
||||||
let s2c_delta = s2c - prev_s2c;
|
let s2c_delta = watchdog_delta(s2c, prev_s2c);
|
||||||
|
|
||||||
if c2s_delta > 0 || s2c_delta > 0 {
|
if c2s_delta > 0 || s2c_delta > 0 {
|
||||||
let secs = WATCHDOG_INTERVAL.as_secs_f64();
|
let secs = WATCHDOG_INTERVAL.as_secs_f64();
|
||||||
@@ -657,7 +683,33 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "relay_security_tests.rs"]
|
#[path = "tests/relay_adversarial_tests.rs"]
|
||||||
mod security_tests;
|
|
||||||
#[path = "relay_adversarial_tests.rs"]
|
|
||||||
mod adversarial_tests;
|
mod adversarial_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_quota_boundary_blackhat_tests.rs"]
|
||||||
|
mod relay_quota_boundary_blackhat_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_quota_model_adversarial_tests.rs"]
|
||||||
|
mod relay_quota_model_adversarial_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_quota_overflow_regression_tests.rs"]
|
||||||
|
mod relay_quota_overflow_regression_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_quota_extended_attack_surface_security_tests.rs"]
|
||||||
|
mod relay_quota_extended_attack_surface_security_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_watchdog_delta_security_tests.rs"]
|
||||||
|
mod relay_watchdog_delta_security_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_atomic_quota_invariant_tests.rs"]
|
||||||
|
mod relay_atomic_quota_invariant_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_baseline_invariant_tests.rs"]
|
||||||
|
mod relay_baseline_invariant_tests;
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::error::ProxyError;
|
|
||||||
use crate::stats::Stats;
|
|
||||||
use crate::stream::BufferPool;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::time::{Duration, Instant, timeout};
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Priority 3: Async Relay HOL Blocking Prevention (OWASP ASVS 5.1.5)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn relay_hol_blocking_prevention_regression() {
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let user = "hol-user";
|
|
||||||
|
|
||||||
let (client_peer, relay_client) = duplex(65536);
|
|
||||||
let (relay_server, server_peer) = duplex(65536);
|
|
||||||
|
|
||||||
let (client_reader, client_writer) = tokio::io::split(relay_client);
|
|
||||||
let (server_reader, server_writer) = tokio::io::split(relay_server);
|
|
||||||
let (mut cp_reader, mut cp_writer) = tokio::io::split(client_peer);
|
|
||||||
let (mut sp_reader, mut sp_writer) = tokio::io::split(server_peer);
|
|
||||||
|
|
||||||
let relay_task = tokio::spawn(relay_bidirectional(
|
|
||||||
client_reader,
|
|
||||||
client_writer,
|
|
||||||
server_reader,
|
|
||||||
server_writer,
|
|
||||||
8192,
|
|
||||||
8192,
|
|
||||||
user,
|
|
||||||
Arc::clone(&stats),
|
|
||||||
None,
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
));
|
|
||||||
|
|
||||||
let payload_size = 1024 * 10;
|
|
||||||
let s2c_payload = vec![0x41; payload_size];
|
|
||||||
let c2s_payload = vec![0x42; payload_size];
|
|
||||||
|
|
||||||
let s2c_handle = tokio::spawn(async move {
|
|
||||||
sp_writer.write_all(&s2c_payload).await.unwrap();
|
|
||||||
|
|
||||||
let mut total_read = 0;
|
|
||||||
let mut buf = [0u8; 10];
|
|
||||||
while total_read < payload_size {
|
|
||||||
let n = cp_reader.read(&mut buf).await.unwrap();
|
|
||||||
total_read += n;
|
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
cp_writer.write_all(&c2s_payload).await.unwrap();
|
|
||||||
|
|
||||||
let mut server_buf = vec![0u8; payload_size];
|
|
||||||
sp_reader.read_exact(&mut server_buf).await.unwrap();
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
|
|
||||||
assert!(elapsed < Duration::from_millis(1000), "C->S must not be blocked by slow S->C (HOL blocking): {:?}", elapsed);
|
|
||||||
assert_eq!(server_buf, c2s_payload);
|
|
||||||
|
|
||||||
s2c_handle.abort();
|
|
||||||
relay_task.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Priority 3: Data Quota Mid-Session Cutoff (OWASP ASVS 5.1.6)
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn relay_quota_mid_session_cutoff() {
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let user = "quota-mid-user";
|
|
||||||
let quota = 5000;
|
|
||||||
|
|
||||||
let (client_peer, relay_client) = duplex(8192);
|
|
||||||
let (relay_server, server_peer) = duplex(8192);
|
|
||||||
|
|
||||||
let (client_reader, client_writer) = tokio::io::split(relay_client);
|
|
||||||
let (server_reader, server_writer) = tokio::io::split(relay_server);
|
|
||||||
let (mut _cp_reader, mut cp_writer) = tokio::io::split(client_peer);
|
|
||||||
let (mut sp_reader, _sp_writer) = tokio::io::split(server_peer);
|
|
||||||
|
|
||||||
let relay_task = tokio::spawn(relay_bidirectional(
|
|
||||||
client_reader,
|
|
||||||
client_writer,
|
|
||||||
server_reader,
|
|
||||||
server_writer,
|
|
||||||
1024,
|
|
||||||
1024,
|
|
||||||
user,
|
|
||||||
Arc::clone(&stats),
|
|
||||||
Some(quota),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Send 4000 bytes (Ok)
|
|
||||||
let buf1 = vec![0x42; 4000];
|
|
||||||
cp_writer.write_all(&buf1).await.unwrap();
|
|
||||||
let mut server_recv = vec![0u8; 4000];
|
|
||||||
sp_reader.read_exact(&mut server_recv).await.unwrap();
|
|
||||||
|
|
||||||
// Send another 2000 bytes (Total 6000 > 5000)
|
|
||||||
let buf2 = vec![0x42; 2000];
|
|
||||||
let _ = cp_writer.write_all(&buf2).await;
|
|
||||||
|
|
||||||
let relay_res = timeout(Duration::from_secs(1), relay_task).await.unwrap();
|
|
||||||
|
|
||||||
match relay_res {
|
|
||||||
Ok(Err(ProxyError::DataQuotaExceeded { .. })) => {
|
|
||||||
// Expected
|
|
||||||
}
|
|
||||||
other => panic!("Expected DataQuotaExceeded error, got: {:?}", other),
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut small_buf = [0u8; 1];
|
|
||||||
let n = sp_reader.read(&mut small_buf).await.unwrap();
|
|
||||||
assert_eq!(n, 0, "Server must see EOF after quota reached");
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
+12
-11
@@ -71,6 +71,12 @@ impl RouteRuntimeController {
|
|||||||
if state.mode == mode {
|
if state.mode == mode {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if matches!(mode, RelayRouteMode::Direct) {
|
||||||
|
self.direct_since_epoch_secs
|
||||||
|
.store(now_epoch_secs(), Ordering::Relaxed);
|
||||||
|
} else {
|
||||||
|
self.direct_since_epoch_secs.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
state.mode = mode;
|
state.mode = mode;
|
||||||
state.generation = state.generation.saturating_add(1);
|
state.generation = state.generation.saturating_add(1);
|
||||||
next = Some(*state);
|
next = Some(*state);
|
||||||
@@ -81,13 +87,6 @@ impl RouteRuntimeController {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(mode, RelayRouteMode::Direct) {
|
|
||||||
self.direct_since_epoch_secs
|
|
||||||
.store(now_epoch_secs(), Ordering::Relaxed);
|
|
||||||
} else {
|
|
||||||
self.direct_since_epoch_secs.store(0, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
next
|
next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,9 +119,7 @@ pub(crate) fn affected_cutover_state(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cutover_stagger_delay(session_id: u64, generation: u64) -> Duration {
|
pub(crate) fn cutover_stagger_delay(session_id: u64, generation: u64) -> Duration {
|
||||||
let mut value = session_id
|
let mut value = session_id ^ generation.rotate_left(17) ^ 0x9e37_79b9_7f4a_7c15;
|
||||||
^ generation.rotate_left(17)
|
|
||||||
^ 0x9e37_79b9_7f4a_7c15;
|
|
||||||
value ^= value >> 30;
|
value ^= value >> 30;
|
||||||
value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||||
value ^= value >> 27;
|
value ^= value >> 27;
|
||||||
@@ -133,5 +130,9 @@ pub(crate) fn cutover_stagger_delay(session_id: u64, generation: u64) -> Duratio
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "route_mode_security_tests.rs"]
|
#[path = "tests/route_mode_security_tests.rs"]
|
||||||
mod security_tests;
|
mod security_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/route_mode_coherence_adversarial_tests.rs"]
|
||||||
|
mod coherence_adversarial_tests;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
/// Session eviction is intentionally disabled in runtime.
|
||||||
|
///
|
||||||
|
/// The initial `user+dc` single-lease model caused valid parallel client
|
||||||
|
/// connections to evict each other. Keep the API shape for compatibility,
|
||||||
|
/// but make it a no-op until a safer policy is introduced.
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct SessionLease;
|
||||||
|
|
||||||
|
impl SessionLease {
|
||||||
|
pub fn is_stale(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn release(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RegistrationResult {
|
||||||
|
pub lease: SessionLease,
|
||||||
|
pub replaced_existing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_session(_user: &str, _dc_idx: i16) -> RegistrationResult {
|
||||||
|
RegistrationResult {
|
||||||
|
lease: SessionLease,
|
||||||
|
replaced_existing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_session_eviction_disabled_behavior() {
|
||||||
|
let first = register_session("alice", 2);
|
||||||
|
let second = register_session("alice", 2);
|
||||||
|
assert!(!first.replaced_existing);
|
||||||
|
assert!(!second.replaced_existing);
|
||||||
|
assert!(!first.lease.is_stale());
|
||||||
|
assert!(!second.lease.is_stale());
|
||||||
|
first.lease.release();
|
||||||
|
second.lease.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user