Merge pull request #100 from vladon/feature/announce-hostname
feat: extend announce_ip to accept hostnames
This commit is contained in:
58
.kilocode/rules-architect/AGENTS.md
Normal file
58
.kilocode/rules-architect/AGENTS.md
Normal file
@@ -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
|
||||||
23
.kilocode/rules-code/AGENTS.md
Normal file
23
.kilocode/rules-code/AGENTS.md
Normal file
@@ -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<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
|
||||||
27
.kilocode/rules-debug/AGENTS.md
Normal file
27
.kilocode/rules-debug/AGENTS.md
Normal file
@@ -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
|
||||||
37
AGENTS.md
Normal file
37
AGENTS.md
Normal file
@@ -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<T,R,W>`](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
|
||||||
@@ -497,6 +497,12 @@ pub struct UpstreamConfig {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ListenerConfig {
|
pub struct ListenerConfig {
|
||||||
pub ip: IpAddr,
|
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<String>,
|
||||||
|
/// Deprecated: Use `announce` instead. IP address to announce in proxy links.
|
||||||
|
/// Migrated to `announce` automatically if `announce` is not set.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub announce_ip: Option<IpAddr>,
|
pub announce_ip: Option<IpAddr>,
|
||||||
}
|
}
|
||||||
@@ -716,6 +722,7 @@ impl ProxyConfig {
|
|||||||
if let Ok(ipv4) = ipv4_str.parse::<IpAddr>() {
|
if let Ok(ipv4) = ipv4_str.parse::<IpAddr>() {
|
||||||
config.server.listeners.push(ListenerConfig {
|
config.server.listeners.push(ListenerConfig {
|
||||||
ip: ipv4,
|
ip: ipv4,
|
||||||
|
announce: None,
|
||||||
announce_ip: None,
|
announce_ip: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -723,12 +730,20 @@ impl ProxyConfig {
|
|||||||
if let Ok(ipv6) = ipv6_str.parse::<IpAddr>() {
|
if let Ok(ipv6) = ipv6_str.parse::<IpAddr>() {
|
||||||
config.server.listeners.push(ListenerConfig {
|
config.server.listeners.push(ListenerConfig {
|
||||||
ip: ipv6,
|
ip: ipv6,
|
||||||
|
announce: None,
|
||||||
announce_ip: 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
|
// Migration: show_link (top-level) → general.links.show
|
||||||
if !config.show_link.is_empty() && config.general.links.show.is_empty() {
|
if !config.show_link.is_empty() && config.general.links.show.is_empty() {
|
||||||
config.general.links.show = config.show_link.clone();
|
config.general.links.show = config.show_link.clone();
|
||||||
|
|||||||
18
src/main.rs
18
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())?;
|
let listener = TcpListener::from_std(socket.into())?;
|
||||||
info!("Listening on {}", addr);
|
info!("Listening on {}", addr);
|
||||||
|
|
||||||
let public_ip = if let Some(ip) = listener_conf.announce_ip {
|
// Resolve the public host for link generation
|
||||||
ip
|
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() {
|
} else if listener_conf.ip.is_unspecified() {
|
||||||
|
// Auto-detect for unspecified addresses
|
||||||
if listener_conf.ip.is_ipv4() {
|
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 {
|
} 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 {
|
} else {
|
||||||
listener_conf.ip
|
listener_conf.ip.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show per-listener proxy links only when public_host is not set
|
// 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() {
|
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);
|
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);
|
listeners.push(listener);
|
||||||
|
|||||||
Reference in New Issue
Block a user