diff --git a/Cargo.lock b/Cargo.lock index 8a33434a..e33b6360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2112,7 +2112,7 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "pony" -version = "0.2.4" +version = "0.2.5" dependencies = [ "anyhow", "async-trait", @@ -2131,6 +2131,7 @@ dependencies = [ "log", "netstat2", "openssl", + "percent-encoding", "postgres-types", "prost", "prost-derive", diff --git a/Cargo.toml b/Cargo.toml index 38d7ee57..0cfbba0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pony" -version = "0.2.4" +version = "0.2.5" edition = "2021" build = "build.rs" @@ -32,6 +32,7 @@ ipnet = { version = "2", features=["serde"] } log = "0.4" netstat2 = { version = "0.11.1" } openssl = { version = "0.10", features = ["vendored"] } +percent-encoding = "2" prost = { version = "0.13" } prost-derive = { version = "0.13" } rand = "0.8" diff --git a/dev/dev.sql b/dev/dev.sql index cfa4cf40..6845f1d0 100644 --- a/dev/dev.sql +++ b/dev/dev.sql @@ -198,4 +198,9 @@ ALTER TABLE connections ADD COLUMN token UUID DEFAULT NULL; ALTER TABLE inbounds ADD COLUMN h2 JSONB DEFAULT NULL; +ALTER TABLE inbounds ADD COLUMN mtproto_secret TEXT DEFAULT NULL; + +ALTER TYPE proto ADD VALUE 'mtproto'; + + diff --git a/src/bin/agent/core/service.rs b/src/bin/agent/core/service.rs index 5eba29f2..b21f55f2 100644 --- a/src/bin/agent/core/service.rs +++ b/src/bin/agent/core/service.rs @@ -120,6 +120,13 @@ pub async fn run(settings: AgentSettings) -> Result<()> { None }; + // Init Mtproto + let mtproto_config = if settings.mtproto.enabled { + Some(settings.mtproto.clone()) + } else { + None + }; + let subscriber = ZmqSubscriber::new( &settings.zmq.endpoint, &settings.node.uuid, @@ -127,7 +134,13 @@ pub async fn run(settings: AgentSettings) -> Result<()> { ); let node_config = NodeConfig::from_raw(settings.node.clone()); - let node = Node::new(node_config?, xray_config, wg_config.clone(), h2_config); + let node = Node::new( + node_config?, + xray_config, + wg_config.clone(), + h2_config, + mtproto_config, + ); let memory: Arc> = Arc::new(RwLock::new(MemoryCache::with_node(node.clone()))); diff --git a/src/bin/agent/core/tasks.rs b/src/bin/agent/core/tasks.rs index 6351ee05..c45e1a9b 100644 --- a/src/bin/agent/core/tasks.rs +++ b/src/bin/agent/core/tasks.rs @@ -247,6 +247,9 @@ where Tag::Hysteria2 => { return Err(PonyError::Custom("Hysteria2 is not supported".into())) } + Tag::Mtproto => { + return Err(PonyError::Custom("Mtproto is not supported".into())) + } } } @@ -299,6 +302,9 @@ where Tag::Hysteria2 => { return Err(PonyError::Custom("Hysteria2 is not supported".into())) } + Tag::Mtproto => { + return Err(PonyError::Custom("Mtproto is not supported".into())) + } } } diff --git a/src/bin/api/core/http/handlers/connection.rs b/src/bin/api/core/http/handlers/connection.rs index b018b8d2..76f200a4 100644 --- a/src/bin/api/core/http/handlers/connection.rs +++ b/src/bin/api/core/http/handlers/connection.rs @@ -307,6 +307,7 @@ where ProtoTag::Hysteria2 => Proto::Hysteria2 { token: conn_req.token.unwrap(), }, + ProtoTag::Mtproto => unreachable!("Mtproto handled earlier"), }; drop(mem); diff --git a/src/bin/api/core/http/handlers/html.rs b/src/bin/api/core/http/handlers/html.rs new file mode 100644 index 00000000..21195f48 --- /dev/null +++ b/src/bin/api/core/http/handlers/html.rs @@ -0,0 +1,167 @@ +pub static HEAD: &str = r#" + + + +Информация о подписке + +"#; + +pub static FOOTER: &str = r#" +"#; diff --git a/src/bin/api/core/http/handlers/mod.rs b/src/bin/api/core/http/handlers/mod.rs index 7f58ded9..080c3bf6 100644 --- a/src/bin/api/core/http/handlers/mod.rs +++ b/src/bin/api/core/http/handlers/mod.rs @@ -1,4 +1,5 @@ pub mod connection; +pub mod html; pub mod node; pub mod sub; diff --git a/src/bin/api/core/http/handlers/sub.rs b/src/bin/api/core/http/handlers/sub.rs index 1eed8fe8..47a231e9 100644 --- a/src/bin/api/core/http/handlers/sub.rs +++ b/src/bin/api/core/http/handlers/sub.rs @@ -1,7 +1,10 @@ use base64::Engine; use chrono::DateTime; use chrono::Utc; +use pony::mtproto_op::mtproto_conn; +use url::Url; +use pony::http::requests::MtprotoQueryParam; use pony::http::requests::TagReq; use warp::http::Response; use warp::http::StatusCode; @@ -28,6 +31,7 @@ use pony::SubscriptionOp; use pony::SubscriptionStorageOp; use pony::Tag; +use super::html::{FOOTER, HEAD}; use crate::core::sync::tasks::SyncOp; use crate::core::sync::MemSync; @@ -292,102 +296,22 @@ where } else { format!("0") }; + let base_link = format!("{}/sub?id={}&env={}", host, id, env); - let main_link = format!("{}/sub?id={}&format=txt&env={}", host, id, env); + let base_link_mtproto = format!("{}/sub/mtproto?id={}&env={}", host, id, env); + let main_link_vless = format!("{}/sub?id={}&format=txt&env={}&proto=Xray", host, id, env); + let main_link_h2 = format!( + "{}/sub?id={}&format=txt&env={}&proto=Hysteria2", + host, id, env + ); let html = format!( -r#" - - - -Информация о подписке - - +r#"{head}

Подписка на Рилзопровод

-
Статус: {status_text}
Дата окончания: {expires}
Осталось дней: {days}
@@ -401,45 +325,64 @@ ul {{

Ссылки для подключения

-Xray Универсальная ссылка (рекомендуется) +

Xray Vless

+ +Универсальная ссылка

+

Дополнительные форматы:

+
-Hysteria2(Beta) Универсальная ссылка (рекомендуется) -
- +
+ -

Дополнительные форматы Xray:

-
    -
  • TXT для v2ray
  • -
  • Clash — для Clash / Clash Meta
  • -
+
Отсканируйте в приложении
+
+ +

Поддерживаемые приложения

-

Дополнительные форматы Hysteria2:

+


+
+
+

Hysteria2(Beta)

+ +Универсальная ссылка +
+ + +

Дополнительные форматы:

+ + +
+ + +
Отсканируйте в приложении
+

Поддерживаемые приложения

+ -
+

-
-

QR-код

- +

BONUS TRACK: Mtproto(tg-proxy)

-
Отсканируйте в VPN-приложении
-
+Открыть +



@@ -448,7 +391,7 @@ Hysteria2(Beta) Скопировать код
-
Вы пригласили: {invited}
+
Вы пригласили: {invited}
Вы получили {bonus_days} бесплатных дней
Информация о реферальной программе
@@ -457,12 +400,7 @@ Hysteria2(Beta) @@ -475,7 +413,13 @@ function copyText(text) {{ window.onload = () => {{ QRCode.toCanvas( document.getElementById("qr"), - "{main_link}", + "{main_link_vless}", + {{ width: 220 }} + ); + + QRCode.toCanvas( + document.getElementById("qr2"), + "{main_link_h2}", {{ width: 220 }} ); }}; @@ -483,6 +427,8 @@ window.onload = () => {{ "#, + head = HEAD, + footer = FOOTER, status_class = status_class, status_text = status_text, expires = expires, @@ -629,6 +575,11 @@ where tags.push(Tag::Hysteria2); tags } + + TagReq::Mtproto => { + tags.push(Tag::Mtproto); + tags + } }; if let Some(conns) = conns { @@ -743,3 +694,99 @@ where } } } + +/// Gets Subscriprion link +// GET /sub/mtproto?id= +pub async fn mtproto_link_handler( + param: MtprotoQueryParam, + memory: MemSync, +) -> Result, warp::Rejection> +where + N: NodeStorageOp + Sync + Send + Clone + 'static, + C: ConnectionApiOp + + ConnectionBaseOp + + Sync + + Send + + Clone + + 'static + + From + + std::fmt::Debug + + PartialEq, + S: SubscriptionOp + Send + Sync + Clone + 'static + PartialEq, +{ + let mem = memory.memory.read().await; + + let nodes = mem.nodes.get_by_env(¶m.env); + + if mem.subscriptions.get(¶m.id).is_none() { + return Ok(Box::new(http::not_found(&format!( + "Subscription {} is not found", + param.id + )))); + }; + + let links: Vec = nodes + .iter() + .flat_map(|node_vec| node_vec.iter()) + .filter_map(|node| { + node.inbounds + .values() + .find(|inb| inb.tag == Tag::Mtproto) + .and_then(|inbound| mtproto_conn(node.address, inbound, &node.label).ok()) + }) + .collect(); + + let html_links = links + .iter() + .filter_map(|l| { + let url = Url::parse(l).ok()?; + + let label = url + .fragment() + .map(|f| { + percent_encoding::percent_decode_str(f) + .decode_utf8_lossy() + .to_string() + }) + .unwrap_or_else(|| "Telegram Proxy".into()); + + Some(format!( + "
  • + {label} + Connect +
  • ", + href = l, + label = label + )) + }) + .collect::>() + .join("\n"); + + let html = format!( + r#"{head} + + +
    +

    Mtproto (tg-proxy)

    + +
    + +

    Ссылки для подключения

    + +
      {html_links}
    +


    +
    +{footer} + + +"#, + head = HEAD, + footer = FOOTER, + html_links = html_links + ); + + Ok(Box::new(warp::reply::with_status( + warp::reply::with_header(html, "Content-Type", "text/html; charset=utf-8"), + StatusCode::OK, + ))) +} diff --git a/src/bin/api/core/http/routes.rs b/src/bin/api/core/http/routes.rs index 5e2f9851..08647a7d 100644 --- a/src/bin/api/core/http/routes.rs +++ b/src/bin/api/core/http/routes.rs @@ -121,6 +121,14 @@ where .and(with_state(self.sync.clone())) .and_then(subscription_link_handler); + let get_mtproto_links = warp::get() + .and(warp::path("sub")) + .and(warp::path("mtproto")) + .and(warp::path::end()) + .and(warp::query::()) + .and(with_state(self.sync.clone())) + .and_then(mtproto_link_handler); + let get_subscription_info_route = warp::get() .and(warp::path("sub")) .and(warp::path("info")) @@ -203,6 +211,7 @@ where .or(get_subscription_info_route) .or(post_subscription_route) .or(put_subscription_route) + .or(get_mtproto_links) // Node .or(get_nodes_route) .or(get_node_route) diff --git a/src/bin/api/core/postgres/node.rs b/src/bin/api/core/postgres/node.rs index ba6a9bce..abeda38f 100644 --- a/src/bin/api/core/postgres/node.rs +++ b/src/bin/api/core/postgres/node.rs @@ -79,12 +79,12 @@ impl PgNode { INSERT INTO inbounds ( id, node_id, tag, port, stream_settings, uplink, downlink, conn_count, - wg_pubkey, wg_privkey, wg_interface, wg_network, wg_address, dns, h2 + wg_pubkey, wg_privkey, wg_interface, wg_network, wg_address, dns, h2, mtproto_secret ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, - $9, $10, $11, $12, $13, $14, $15 + $9, $10, $11, $12, $13, $14, $15, $16 ) ON CONFLICT (node_id, tag) DO UPDATE SET port = EXCLUDED.port, @@ -98,7 +98,8 @@ impl PgNode { wg_network = EXCLUDED.wg_network, wg_address = EXCLUDED.wg_address, dns = EXCLUDED.dns, - h2 = EXCLUDED.h2 + h2 = EXCLUDED.h2, + mtproto_secret = EXCLUDED.mtproto_secret "; for inbound in node.inbounds.values() { @@ -145,6 +146,7 @@ impl PgNode { &wg_address, &dns, &h2_settings, + &inbound.mtproto_secret, ], ) .await?; @@ -164,7 +166,7 @@ impl PgNode { n.id AS node_id, n.uuid, n.env, n.hostname, n.address, n.status, n.created_at, n.modified_at, n.label, n.interface, n.cores, n.max_bandwidth_bps, i.id AS inbound_id, i.tag, i.port, i.stream_settings, i.uplink, i.downlink, - i.conn_count, i.wg_pubkey, i.wg_privkey, i.wg_interface, i.wg_network, i.wg_address, i.dns, i.h2 + i.conn_count, i.wg_pubkey, i.wg_privkey, i.wg_interface, i.wg_network, i.wg_address, i.dns, i.h2, i.mtproto_secret FROM nodes n LEFT JOIN inbounds i ON n.id = i.node_id", &[], @@ -254,6 +256,8 @@ impl PgNode { _ => None, }; + let mtproto_secret = row.get::<_, Option>("mtproto_secret"); + let inbound = Inbound { tag: row.get("tag"), port: row.get::<_, i32>("port") as u16, @@ -267,6 +271,7 @@ impl PgNode { conn_count: row.get("conn_count"), wg, h2, + mtproto_secret, }; node_entry.inbounds.insert(inbound.tag, inbound); diff --git a/src/bin/auth/core/service.rs b/src/bin/auth/core/service.rs index e3ca555f..6356da27 100644 --- a/src/bin/auth/core/service.rs +++ b/src/bin/auth/core/service.rs @@ -27,7 +27,7 @@ pub async fn run(settings: AuthServiceSettings) -> Result<()> { let (shutdown_tx, _) = broadcast::channel::<()>(1); let node_config = NodeConfig::from_raw(settings.node.clone()); - let node = Node::new(node_config?, None, None, None); + let node = Node::new(node_config?, None, None, None, None); let memory: Arc> = Arc::new(RwLock::new(MemoryCache::with_node(node.clone()))); diff --git a/src/config/settings.rs b/src/config/settings.rs index 08755319..81eda585 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -3,6 +3,7 @@ use crate::Result; use default_net::{get_default_interface, get_interfaces}; use serde::de::DeserializeOwned; use serde::Deserialize; +use serde::Serialize; use std::env; use std::fs; use std::net::Ipv4Addr; @@ -145,6 +146,10 @@ fn default_h2_config_path() -> String { "dev/h2.yaml".to_string() } +fn default_mtproto_port() -> u16 { + 8443 +} + #[derive(Clone, Debug, Deserialize, Default)] pub struct ApiServiceConfig { #[serde(default = "default_api_web_listen")] @@ -405,6 +410,15 @@ pub struct H2Config { pub path: String, } +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct MtprotoConfig { + #[serde(default = "default_disabled")] + pub enabled: bool, + #[serde(default = "default_mtproto_port")] + pub port: u16, + pub secret: String, +} + #[derive(Clone, Debug, Deserialize, Default)] pub struct ZmqSubscriberConfig { #[serde(default = "default_zmq_sub_endpoint")] @@ -512,6 +526,8 @@ pub struct AgentSettings { #[serde(default)] pub h2: H2Config, #[serde(default)] + pub mtproto: MtprotoConfig, + #[serde(default)] pub zmq: ZmqSubscriberConfig, #[serde(default)] pub node: NodeConfigRaw, diff --git a/src/config/xray.rs b/src/config/xray.rs index b1cd8d52..ac2d64c6 100644 --- a/src/config/xray.rs +++ b/src/config/xray.rs @@ -83,6 +83,7 @@ pub struct Inbound { pub conn_count: Option, pub wg: Option, pub h2: Option, + pub mtproto_secret: Option, } impl Inbound { @@ -93,6 +94,7 @@ impl Inbound { tag: self.tag, wg: self.wg.clone(), h2: self.h2.clone(), + mtproto_secret: self.mtproto_secret.clone(), } } diff --git a/src/http/requests.rs b/src/http/requests.rs index bbb281b8..90a9598a 100644 --- a/src/http/requests.rs +++ b/src/http/requests.rs @@ -29,6 +29,7 @@ pub enum TagReq { Xray, Wireguard, Hysteria2, + Mtproto, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct SubIdQueryParam { @@ -47,6 +48,13 @@ pub struct SubQueryParam { pub proto: TagReq, } +#[derive(Debug, Deserialize)] +pub struct MtprotoQueryParam { + pub id: uuid::Uuid, + #[serde(default = "default_env")] + pub env: String, +} + #[derive(Debug, Deserialize)] pub struct SubCreateReq { pub referred_by: Option, @@ -136,6 +144,7 @@ pub struct InboundResponse { pub stream_settings: Option, pub wg: Option, pub h2: Option, + pub mtproto_secret: Option, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -152,6 +161,9 @@ pub struct ConnCreateRequest { impl ConnCreateRequest { pub fn validate(&self) -> Result<(), String> { + if self.proto == Tag::Mtproto { + return Err("Mtproto is not supported for Connection".into()); + } if self.password.is_some() && self.wg.is_some() { return Err("Cannot specify both password and wg".into()); } diff --git a/src/lib.rs b/src/lib.rs index a5413017..61f70432 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod h2_op; pub mod http; pub mod memory; pub mod metrics; +pub mod mtproto_op; pub mod utils; pub mod wireguard_op; pub mod xray_api; diff --git a/src/memory/connection/proto.rs b/src/memory/connection/proto.rs index 85d36f14..35a8bb0d 100644 --- a/src/memory/connection/proto.rs +++ b/src/memory/connection/proto.rs @@ -14,6 +14,7 @@ pub enum Proto { Shadowsocks { password: String }, Xray(Tag), Hysteria2 { token: uuid::Uuid }, + Mtproto { secret: String }, } impl Proto { @@ -23,6 +24,7 @@ impl Proto { Proto::Shadowsocks { .. } => Tag::Shadowsocks, Proto::Hysteria2 { .. } => Tag::Hysteria2, Proto::Xray(tag) => *tag, + Proto::Mtproto { .. } => unreachable!(), } } diff --git a/src/memory/node.rs b/src/memory/node.rs index 6a903adb..7c0da092 100644 --- a/src/memory/node.rs +++ b/src/memory/node.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use super::tag::ProtoTag as Tag; use crate::config::h2::H2Settings; -use crate::config::settings::NodeConfig; +use crate::config::settings::{MtprotoConfig, NodeConfig}; use crate::config::wireguard::WireguardSettings; use crate::config::xray::{Config as XrayConfig, Inbound}; use crate::http::requests::NodeResponse; @@ -74,6 +74,7 @@ impl Node { xray_config: Option, wg_config: Option, h2_config: Option, + mtproto_config: Option, ) -> Self { let now = Utc::now(); let mut inbounds: HashMap = HashMap::new(); @@ -101,6 +102,24 @@ impl Node { conn_count: None, wg: wg_config, h2: None, + mtproto_secret: None, + }, + ); + } + + if let Some(ref config) = mtproto_config { + inbounds.insert( + Tag::Mtproto, + Inbound { + port: config.port, + tag: Tag::Mtproto, + stream_settings: None, + uplink: None, + downlink: None, + conn_count: None, + wg: None, + h2: None, + mtproto_secret: Some(config.secret.clone()), }, ); } @@ -117,6 +136,7 @@ impl Node { conn_count: None, wg: None, h2: h2_config, + mtproto_secret: None, }, ); } @@ -190,6 +210,10 @@ impl Node { Err(format!("Inbound {} not found", tag)) } } + + pub fn inbound(&self, tag: Tag) -> Option<&Inbound> { + self.inbounds.values().find(|i| i.tag == tag) + } } pub struct Stat { diff --git a/src/memory/tag.rs b/src/memory/tag.rs index 222f6163..969c56fa 100644 --- a/src/memory/tag.rs +++ b/src/memory/tag.rs @@ -38,6 +38,8 @@ pub enum ProtoTag { Wireguard, #[serde(rename = "Hysteria2")] Hysteria2, + #[serde(rename = "Mtproto")] + Mtproto, } impl fmt::Display for ProtoTag { @@ -50,6 +52,7 @@ impl fmt::Display for ProtoTag { ProtoTag::Shadowsocks => write!(f, "Shadowsocks"), ProtoTag::Wireguard => write!(f, "Wireguard"), ProtoTag::Hysteria2 => write!(f, "Hysteria2"), + ProtoTag::Mtproto => write!(f, "Mtproto"), } } } @@ -65,6 +68,9 @@ impl ProtoTag { pub fn is_hysteria2(&self) -> bool { *self == ProtoTag::Hysteria2 } + pub fn is_mtproto(&self) -> bool { + *self == ProtoTag::Mtproto + } } impl std::str::FromStr for ProtoTag { @@ -79,6 +85,7 @@ impl std::str::FromStr for ProtoTag { "Shadowsocks" => Ok(ProtoTag::Shadowsocks), "Wireguard" => Ok(ProtoTag::Wireguard), "Hysteria2" => Ok(ProtoTag::Hysteria2), + "Mtproto" => Ok(ProtoTag::Mtproto), _ => Err(()), } } diff --git a/src/mtproto_op/mod.rs b/src/mtproto_op/mod.rs new file mode 100644 index 00000000..0a7d3ec1 --- /dev/null +++ b/src/mtproto_op/mod.rs @@ -0,0 +1,23 @@ +use crate::config::xray::Inbound; +use reqwest::Url; +use std::net::Ipv4Addr; + +use crate::PonyError; +use crate::Result; + +pub fn mtproto_conn(address: Ipv4Addr, inbound: &Inbound, label: &str) -> Result { + let port = inbound.port; + + let secret = inbound + .mtproto_secret + .as_ref() + .ok_or(PonyError::Custom("MTProto: secret missing".to_string()))?; + + let mut url = Url::parse(&format!( + "https://t.me/proxy?server={address}&port={port}&secret={secret}" + ))?; + + url.set_fragment(Some(label)); + + Ok(url.to_string()) +}