From 303a6896bf2be7d0f4157ec47d4c4365d46c1390 Mon Sep 17 00:00:00 2001 From: vladon Date: Mon, 16 Feb 2026 16:59:29 +0300 Subject: [PATCH 1/2] AGENTS.md --- .kilocode/rules-architect/AGENTS.md | 58 +++++++++++++++++++++++++++++ .kilocode/rules-code/AGENTS.md | 23 ++++++++++++ .kilocode/rules-debug/AGENTS.md | 27 ++++++++++++++ AGENTS.md | 37 ++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 .kilocode/rules-architect/AGENTS.md create mode 100644 .kilocode/rules-code/AGENTS.md create mode 100644 .kilocode/rules-debug/AGENTS.md create mode 100644 AGENTS.md diff --git a/.kilocode/rules-architect/AGENTS.md b/.kilocode/rules-architect/AGENTS.md new file mode 100644 index 0000000..84e8808 --- /dev/null +++ b/.kilocode/rules-architect/AGENTS.md @@ -0,0 +1,58 @@ +# 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 diff --git a/.kilocode/rules-code/AGENTS.md b/.kilocode/rules-code/AGENTS.md new file mode 100644 index 0000000..df9f664 --- /dev/null +++ b/.kilocode/rules-code/AGENTS.md @@ -0,0 +1,23 @@ +# 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`](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` + +## 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 ` to run tests for specific modules diff --git a/.kilocode/rules-debug/AGENTS.md b/.kilocode/rules-debug/AGENTS.md new file mode 100644 index 0000000..9d390b1 --- /dev/null +++ b/.kilocode/rules-debug/AGENTS.md @@ -0,0 +1,27 @@ +# 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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..55c8f25 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# AGENTS.md + +This file provides guidance to agents when working with code in this repository. + +## Build & Test Commands +```bash +cargo build --release # Production build +cargo test # Run all tests +cargo test --lib error # Run tests for specific module (error module) +cargo bench --bench crypto_bench # Run crypto benchmarks +cargo clippy -- -D warnings # Lint with clippy +``` + +## Project-Specific Conventions + +### Rust Edition +- Uses **Rust edition 2024** (not 2021) - specified in Cargo.toml + +### Error Handling Pattern +- Custom [`Recoverable`](src/error.rs:110) trait distinguishes recoverable vs fatal errors +- [`HandshakeResult`](src/error.rs:292) returns streams on bad client for masking - do not drop them +- Always use [`ProxyError`](src/error.rs:168) from [`src/error.rs`](src/error.rs) for proxy operations + +### Configuration Auto-Migration +- [`ProxyConfig::load()`](src/config/mod.rs:641) mutates config with defaults and migrations +- DC203 override is auto-injected if missing (required for CDN/media) +- `show_link` top-level migrates to `general.links.show` + +### Middle-End Proxy Requirements +- Requires public IP on interface OR 1:1 NAT with STUN probing +- Falls back to direct mode on STUN/interface mismatch unless `stun_iface_mismatch_ignore=true` +- Proxy-secret from Telegram is separate from user secrets + +### TLS Fronting Behavior +- Invalid handshakes are transparently proxied to `mask_host` for DPI evasion +- `fake_cert_len` is randomized at startup (1024-4096 bytes) +- `mask_unix_sock` and `mask_host` are mutually exclusive From 16b5dc56f0b429da5856a6893b8d25505c35e268 Mon Sep 17 00:00:00 2001 From: vladon Date: Mon, 16 Feb 2026 17:26:46 +0300 Subject: [PATCH 2/2] feat: extend announce_ip to accept hostnames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new 'announce' field to ListenerConfig that accepts both IP addresses and hostnames for proxy link generation. The old 'announce_ip' field is deprecated but still supported via automatic migration. Changes: - Add 'announce: Option' field to ListenerConfig - Add migration logic: announce_ip → announce if announce not set - Update main.rs to use announce field for link generation - Support both hostnames (e.g., 'proxy.example.com') and IPs Backward compatible: existing configs using announce_ip continue to work. --- src/config/mod.rs | 15 +++++++++++++++ src/main.rs | 18 ++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 42190e6..f9d2131 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -497,6 +497,12 @@ pub struct UpstreamConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ListenerConfig { pub ip: IpAddr, + /// IP address or hostname to announce in proxy links. + /// Takes precedence over `announce_ip` if both are set. + #[serde(default)] + pub announce: Option, + /// Deprecated: Use `announce` instead. IP address to announce in proxy links. + /// Migrated to `announce` automatically if `announce` is not set. #[serde(default)] pub announce_ip: Option, } @@ -716,6 +722,7 @@ impl ProxyConfig { if let Ok(ipv4) = ipv4_str.parse::() { config.server.listeners.push(ListenerConfig { ip: ipv4, + announce: None, announce_ip: None, }); } @@ -723,12 +730,20 @@ impl ProxyConfig { if let Ok(ipv6) = ipv6_str.parse::() { config.server.listeners.push(ListenerConfig { ip: ipv6, + announce: None, announce_ip: None, }); } } } + // Migration: announce_ip → announce for each listener + for listener in &mut config.server.listeners { + if listener.announce.is_none() && listener.announce_ip.is_some() { + listener.announce = Some(listener.announce_ip.unwrap().to_string()); + } + } + // Migration: show_link (top-level) → general.links.show if !config.show_link.is_empty() && config.general.links.show.is_empty() { config.general.links.show = config.show_link.clone(); diff --git a/src/main.rs b/src/main.rs index 4a6d43d..ba00879 100644 --- a/src/main.rs +++ b/src/main.rs @@ -563,22 +563,28 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai let listener = TcpListener::from_std(socket.into())?; info!("Listening on {}", addr); - let public_ip = if let Some(ip) = listener_conf.announce_ip { - ip + // Resolve the public host for link generation + let public_host = if let Some(ref announce) = listener_conf.announce { + announce.clone() // Use announce (IP or hostname) if explicitly set } else if listener_conf.ip.is_unspecified() { + // Auto-detect for unspecified addresses if listener_conf.ip.is_ipv4() { - detected_ip.ipv4.unwrap_or(listener_conf.ip) + detected_ip.ipv4 + .map(|ip| ip.to_string()) + .unwrap_or_else(|| listener_conf.ip.to_string()) } else { - detected_ip.ipv6.unwrap_or(listener_conf.ip) + detected_ip.ipv6 + .map(|ip| ip.to_string()) + .unwrap_or_else(|| listener_conf.ip.to_string()) } } else { - listener_conf.ip + listener_conf.ip.to_string() }; // Show per-listener proxy links only when public_host is not set if config.general.links.public_host.is_none() && !config.general.links.show.is_empty() { let link_port = config.general.links.public_port.unwrap_or(config.server.port); - print_proxy_links(&public_ip.to_string(), link_port, &config); + print_proxy_links(&public_host, link_port, &config); } listeners.push(listener);