From bc432f06e27dd664113cf50db4934a006fb93878 Mon Sep 17 00:00:00 2001 From: sintanial Date: Sun, 1 Mar 2026 13:53:50 +0300 Subject: [PATCH] Add per-user ad_tag with global fallback and hot-reload - Per-user ad_tag in [access.user_ad_tags], global fallback in general.ad_tag - User tag overrides global; if no user tag, general.ad_tag is used - Both general.ad_tag and user_ad_tags support hot-reload (no restart) --- Cargo.lock | 2 +- README.md | 8 +++--- config.toml | 2 ++ src/config/hot_reload.rs | 42 ++++++++++++++++-------------- src/config/load.rs | 4 +-- src/config/types.rs | 12 ++++++--- src/main.rs | 2 +- src/proxy/middle_relay.rs | 19 +++++++++++++- src/transport/middle_proxy/send.rs | 5 +++- 9 files changed, 65 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 251f0b7..e29b473 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2087,7 +2087,7 @@ dependencies = [ [[package]] name = "telemt" -version = "3.0.13" +version = "3.1.3" dependencies = [ "aes", "anyhow", diff --git a/README.md b/README.md index 093f2cd..8ea25e7 100644 --- a/README.md +++ b/README.md @@ -215,10 +215,12 @@ hello = "00000000000000000000000000000000" ``` ### Advanced -#### Adtag -To use channel advertising and usage statistics from Telegram, get Adtag from [@mtproxybot](https://t.me/mtproxybot), add this parameter to section `[General]` +#### Adtag (per-user) +To use channel advertising and usage statistics from Telegram, get an Adtag from [@mtproxybot](https://t.me/mtproxybot). Set it per user in `[access.user_ad_tags]` (32 hex chars): ```toml -ad_tag = "00000000000000000000000000000000" # Replace zeros to your adtag from @mtproxybot +[access.user_ad_tags] +username1 = "11111111111111111111111111111111" # Replace with your tag from @mtproxybot +username2 = "22222222222222222222222222222222" ``` #### Listening and Announce IPs To specify listening address and/or address in links, add to section `[[server.listeners]]` of config.toml: diff --git a/config.toml b/config.toml index b280234..cb33e3d 100644 --- a/config.toml +++ b/config.toml @@ -5,7 +5,9 @@ # === General Settings === [general] use_middle_proxy = false +# Global ad_tag fallback when user has no per-user tag in [access.user_ad_tags] # ad_tag = "00000000000000000000000000000000" +# Per-user ad_tag in [access.user_ad_tags] (32 hex from @MTProxybot) # === Log Level === # Log level: debug | verbose | normal | silent diff --git a/src/config/hot_reload.rs b/src/config/hot_reload.rs index eec6b8c..e16cff2 100644 --- a/src/config/hot_reload.rs +++ b/src/config/hot_reload.rs @@ -4,21 +4,22 @@ //! //! # What can be reloaded without restart //! -//! | Section | Field | Effect | -//! |-----------|-------------------------------|-----------------------------------| -//! | `general` | `log_level` | Filter updated via `log_level_tx` | -//! | `general` | `ad_tag` | Passed on next connection | -//! | `general` | `middle_proxy_pool_size` | Passed on next connection | -//! | `general` | `me_keepalive_*` | Passed on next connection | -//! | `general` | `desync_all_full` | Applied immediately | -//! | `general` | `update_every` | Applied to ME updater immediately | -//! | `general` | `hardswap` | Applied on next ME map update | -//! | `general` | `me_pool_drain_ttl_secs` | Applied on next ME map update | -//! | `general` | `me_pool_min_fresh_ratio` | Applied on next ME map update | -//! | `general` | `me_reinit_drain_timeout_secs`| Applied on next ME map update | -//! | `general` | `telemetry` / `me_*_policy` | Applied immediately | -//! | `network` | `dns_overrides` | Applied immediately | -//! | `access` | All user/quota fields | Effective immediately | +//! | Section | Field | Effect | +//! |-----------|--------------------------------|------------------------------------------------| +//! | `general` | `log_level` | Filter updated via `log_level_tx` | +//! | `access` | `user_ad_tags` | Passed on next connection | +//! | `general` | `ad_tag` | Passed on next connection (fallback per-user) | +//! | `general` | `middle_proxy_pool_size` | Passed on next connection | +//! | `general` | `me_keepalive_*` | Passed on next connection | +//! | `general` | `desync_all_full` | Applied immediately | +//! | `general` | `update_every` | Applied to ME updater immediately | +//! | `general` | `hardswap` | Applied on next ME map update | +//! | `general` | `me_pool_drain_ttl_secs` | Applied on next ME map update | +//! | `general` | `me_pool_min_fresh_ratio` | Applied on next ME map update | +//! | `general` | `me_reinit_drain_timeout_secs` | Applied on next ME map update | +//! | `general` | `telemetry` / `me_*_policy` | Applied immediately | +//! | `network` | `dns_overrides` | Applied immediately | +//! | `access` | All user/quota fields | Effective immediately | //! //! Fields that require re-binding sockets (`server.port`, `censorship.*`, //! `network.*`, `use_middle_proxy`) are **not** applied; a warning is emitted. @@ -207,14 +208,17 @@ fn log_changes( log_tx.send(new_hot.log_level.clone()).ok(); } - if old_hot.ad_tag != new_hot.ad_tag { + if old_hot.access.user_ad_tags != new_hot.access.user_ad_tags { info!( - "config reload: ad_tag: {} → {}", - old_hot.ad_tag.as_deref().unwrap_or("none"), - new_hot.ad_tag.as_deref().unwrap_or("none"), + "config reload: user_ad_tags updated ({} entries)", + new_hot.access.user_ad_tags.len(), ); } + if old_hot.ad_tag != new_hot.ad_tag { + info!("config reload: general.ad_tag updated (applied on next connection)"); + } + if old_hot.dns_overrides != new_hot.dns_overrides { info!( "config reload: network.dns_overrides updated ({} entries)", diff --git a/src/config/load.rs b/src/config/load.rs index 3aafda2..f37791d 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -532,7 +532,7 @@ impl ProxyConfig { ))); } - if let Some(tag) = &self.general.ad_tag { + for (user, tag) in &self.access.user_ad_tags { let zeros = "00000000000000000000000000000000"; if !is_valid_ad_tag(tag) { return Err(ProxyError::Config( @@ -540,7 +540,7 @@ impl ProxyConfig { )); } if tag == zeros { - warn!("ad_tag is all zeros; register a valid proxy tag via @MTProxybot to enable sponsored channel"); + warn!(user = %user, "user ad_tag is all zeros; register a valid proxy tag via @MTProxybot to enable sponsored channel"); } } diff --git a/src/config/types.rs b/src/config/types.rs index 7a3f6e9..716d78f 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -247,14 +247,15 @@ pub struct GeneralConfig { #[serde(default = "default_true")] pub use_middle_proxy: bool, - #[serde(default)] - pub ad_tag: Option, - /// Path to proxy-secret binary file (auto-downloaded if absent). /// Infrastructure secret from https://core.telegram.org/getProxySecret. #[serde(default = "default_proxy_secret_path")] pub proxy_secret_path: Option, + /// Global ad_tag (32 hex chars from @MTProxybot). Fallback when user has no per-user tag in access.user_ad_tags. + #[serde(default)] + pub ad_tag: Option, + /// Public IP override for middle-proxy NAT environments. /// When set, this IP is used in ME key derivation and RPC_PROXY_REQ "our_addr". #[serde(default)] @@ -807,6 +808,10 @@ pub struct AccessConfig { #[serde(default = "default_access_users")] pub users: HashMap, + /// Per-user ad_tag (32 hex chars from @MTProxybot). + #[serde(default)] + pub user_ad_tags: HashMap, + #[serde(default)] pub user_max_tcp_conns: HashMap, @@ -833,6 +838,7 @@ impl Default for AccessConfig { fn default() -> Self { Self { users: default_access_users(), + user_ad_tags: HashMap::new(), user_max_tcp_conns: HashMap::new(), user_expirations: HashMap::new(), user_data_quota: HashMap::new(), diff --git a/src/main.rs b/src/main.rs index 2675509..b910c64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -448,7 +448,7 @@ async fn main() -> std::result::Result<(), Box> { info!("Middle-proxy STUN probing disabled by network.stun_use=false"); } - // ad_tag (proxy_tag) for advertising + // Global ad_tag (pool default). Used when user has no per-user tag in access.user_ad_tags. let proxy_tag = config .general .ad_tag diff --git a/src/proxy/middle_relay.rs b/src/proxy/middle_relay.rs index a4942ba..0690906 100644 --- a/src/proxy/middle_relay.rs +++ b/src/proxy/middle_relay.rs @@ -238,7 +238,22 @@ where stats.increment_user_connects(&user); stats.increment_user_curr_connects(&user); - let proto_flags = proto_flags_for_tag(proto_tag, me_pool.has_proxy_tag()); + // Per-user ad_tag from access.user_ad_tags; fallback to general.ad_tag (hot-reloadable) + let user_tag: Option> = config + .access + .user_ad_tags + .get(&user) + .and_then(|s| hex::decode(s).ok()) + .filter(|v| v.len() == 16); + let global_tag: Option> = config + .general + .ad_tag + .as_ref() + .and_then(|s| hex::decode(s).ok()) + .filter(|v| v.len() == 16); + let effective_tag = user_tag.or(global_tag); + + let proto_flags = proto_flags_for_tag(proto_tag, effective_tag.is_some()); debug!( trace_id = format_args!("0x{:016x}", trace_id), user = %user, @@ -256,6 +271,7 @@ where let (c2me_tx, mut c2me_rx) = mpsc::channel::(C2ME_CHANNEL_CAPACITY); let me_pool_c2me = me_pool.clone(); + let effective_tag = effective_tag; let c2me_sender = tokio::spawn(async move { let mut sent_since_yield = 0usize; while let Some(cmd) = c2me_rx.recv().await { @@ -268,6 +284,7 @@ where translated_local_addr, &payload, flags, + effective_tag.as_deref(), ).await?; sent_since_yield = sent_since_yield.saturating_add(1); if should_yield_c2me_sender(sent_since_yield, !c2me_rx.is_empty()) { diff --git a/src/transport/middle_proxy/send.rs b/src/transport/middle_proxy/send.rs index f68b1b9..65bc43a 100644 --- a/src/transport/middle_proxy/send.rs +++ b/src/transport/middle_proxy/send.rs @@ -18,6 +18,7 @@ use rand::seq::SliceRandom; use super::registry::ConnMeta; impl MePool { + /// Send RPC_PROXY_REQ. `tag_override`: per-user ad_tag (from access.user_ad_tags); if None, uses pool default. pub async fn send_proxy_req( self: &Arc, conn_id: u64, @@ -26,13 +27,15 @@ impl MePool { our_addr: SocketAddr, data: &[u8], proto_flags: u32, + tag_override: Option<&[u8]>, ) -> Result<()> { + let tag = tag_override.or(self.proxy_tag.as_deref()); let payload = build_proxy_req_payload( conn_id, client_addr, our_addr, data, - self.proxy_tag.as_deref(), + tag, proto_flags, ); let meta = ConnMeta {