From 131a694cd86dc0b04e9538c34214f4eab0afcf2b Mon Sep 17 00:00:00 2001 From: Keivan-sf Date: Sat, 26 Jul 2025 20:51:05 +0330 Subject: [PATCH 1/4] Use a shared raw data model for easier implementation --- src/config_models/mod.rs | 40 ++++++++-- src/parser/mod.rs | 132 ++++++++++++++++++++++++++++++--- src/parser/vless/data.rs | 155 ++++++++------------------------------- src/parser/vless/mod.rs | 130 ++++---------------------------- 4 files changed, 199 insertions(+), 258 deletions(-) diff --git a/src/config_models/mod.rs b/src/config_models/mod.rs index c4d6d28..c6f7b49 100644 --- a/src/config_models/mod.rs +++ b/src/config_models/mod.rs @@ -2,17 +2,17 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct VlessUser { - pub id: String, - pub encryption: String, + pub id: Option, + pub encryption: Option, pub flow: Option, pub level: Option, } #[derive(Serialize, Deserialize)] pub struct VlessServerObject { - pub address: String, - pub port: u16, - pub users: Vec, + pub address: Option, + pub port: Option, + pub users: Option>, } #[derive(Serialize, Deserialize)] @@ -175,3 +175,33 @@ pub struct Config { pub outbounds: Vec, pub inbounds: Vec, } + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct RawData { + pub security: Option, + pub sni: Option, + pub fp: Option, + pub pbk: Option, + pub sid: Option, + pub r#type: Option, + pub flow: Option, + pub path: Option, + pub encryption: Option, + pub header_type: Option, + pub host: Option, + pub seed: Option, + pub quic_security: Option, + pub r#key: Option, + pub mode: Option, + pub service_name: Option, + pub authority: Option, + pub slpn: Option, + pub spx: Option, + pub alpn: Option, + pub extra: Option, + pub allowInsecure: Option, + pub uuid: Option, + pub address: Option, + pub port: Option, +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ad0ff87..a44ed2e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,6 +1,9 @@ -use crate::config_models; -use crate::utils::inbound_generator; -use std::process::exit; +use crate::config_models::{ + self, GRPCSettings, KCPSettings, NonHeaderObject, Outbound, OutboundSettings, QuicSettings, + RawData, RealitySettings, StreamSettings, TCPHeader, TCPSettings, TlsSettings, WsSettings, + XHTTPSettings, +}; +use crate::utils::{inbound_generator, parse_raw_json}; mod uri_identifier; mod vless; @@ -31,19 +34,124 @@ pub fn create_config( pub fn create_outbound_object(uri: &str) -> config_models::Outbound { let protocol = uri_identifier::get_uri_protocol(uri); - match protocol { + let (name, data, outbound_settings): (String, RawData, OutboundSettings) = match protocol { Some(uri_identifier::Protocols::Vless) => { - let vless_data = vless::data::get_data(uri); - let outbound_object = vless::create_outbound_object(vless_data); - return outbound_object; + let d = vless::data::get_data(uri); + let s = vless::create_outbound_settings(&d); + (String::from("vless"), d, s) } Some(_) => { - println!("The protocol was recognized but is not supported yet"); - exit(0); + panic!("The protocol was recognized but is not supported yet"); } None => { - println!("The protcol is not supported"); - exit(0); + panic!("The protocol is not supported"); } - } + }; + + let network_type = data.r#type.clone().unwrap_or(String::from("")); + let allow_insecure = data.allowInsecure == Some(String::from("true")) + || data.allowInsecure == Some(String::from("1")); + + let outbound = Outbound { + protocol: name, + tag: String::from("proxy"), + streamSettings: StreamSettings { + network: data.r#type.clone(), + security: data.security.clone(), + tlsSettings: if data.security == Some(String::from("tls")) { + Some(TlsSettings { + alpn: data.alpn.map(|alpn| vec![alpn]), + rejectUnknownSni: None, + enableSessionResumption: None, + minVersion: None, + maxVersion: None, + cipherSuites: None, + disableSystemRoot: None, + preferServerCipherSuites: None, + fingerprint: data.fp.clone(), + serverName: data.sni.clone(), + allowInsecure: allow_insecure, + }) + } else { + None + }, + wsSettings: if network_type == String::from("ws") { + Some(WsSettings { + Host: data.host.clone(), + path: data.path.clone(), + acceptProxyProtocol: None, + }) + } else { + None + }, + tcpSettings: if network_type == String::from("tcp") { + Some(TCPSettings { + header: Some(TCPHeader { + r#type: Some(data.header_type.unwrap_or(String::from("none"))), + }), + acceptProxyProtocol: None, + }) + } else { + None + }, + realitySettings: if network_type == String::from("reality") { + Some(RealitySettings { + publicKey: data.pbk, + serverName: data.sni.clone(), + shortId: data.sid, + spiderX: Some(String::from("")), + fingerprint: data.fp.clone(), + }) + } else { + None + }, + grpcSettings: if network_type == String::from("grpc") { + Some(GRPCSettings { + authority: data.authority, + multiMode: Some(false), + serviceName: data.service_name, + }) + } else { + None + }, + quicSettings: if network_type == String::from("quic") { + Some(QuicSettings { + header: Some(NonHeaderObject { + r#type: Some(String::from("none")), + }), + security: Some(String::from("none")), + key: Some(String::from("")), + }) + } else { + None + }, + kcpSettings: if network_type == String::from("kcp") { + Some(KCPSettings { + mtu: None, + tti: None, + congestion: None, + uplinkCapacity: None, + readBufferSize: None, + writeBufferSize: None, + downlinkCapacity: None, + seed: data.seed, + }) + } else { + None + }, + xhttpSettings: if network_type == String::from("xhttp") { + Some(XHTTPSettings { + host: data.host.clone(), + path: data.path.clone(), + mode: data.mode, + extra: data.extra.and_then(|e| parse_raw_json(e.as_str())), + }) + } else { + None + }, + }, + settings: outbound_settings, + }; + + return outbound; } diff --git a/src/parser/vless/data.rs b/src/parser/vless/data.rs index 21916f2..b39c99d 100644 --- a/src/parser/vless/data.rs +++ b/src/parser/vless/data.rs @@ -1,19 +1,44 @@ +use crate::config_models::RawData; use crate::parser::vless::models; use crate::utils::{get_parameter_value, url_decode}; use http::Uri; -pub fn get_data(uri: &str) -> models::VlessData { +pub fn get_data(uri: &str) -> RawData { let data = uri.split_once("vless://").unwrap().1; let query_and_name = uri.split_once("?").unwrap().1; - let query = query_and_name + let raw_query = query_and_name .split_once("#") .unwrap_or((query_and_name, "")) .0; - let parsed_query = parse_vless_query(query); let parsed_address = parse_vless_address(data.split_once("?").unwrap().0); - return models::VlessData { - query: parsed_query, - address_data: parsed_address, + let query: Vec<(&str, &str)> = querystring::querify(raw_query); + + return RawData { + uuid: Some(parsed_address.uuid), + port: Some(parsed_address.port), + address: Some(parsed_address.address), + alpn: url_decode(get_parameter_value(&query, "alpn")), + path: url_decode(get_parameter_value(&query, "path")), + authority: url_decode(get_parameter_value(&query, "authority")), + pbk: url_decode(get_parameter_value(&query, "pbk")), + security: get_parameter_value(&query, "security"), + sid: url_decode(get_parameter_value(&query, "sid")), + flow: get_parameter_value(&query, "flow"), + sni: get_parameter_value(&query, "sni"), + fp: url_decode(get_parameter_value(&query, "fp")), + r#type: get_parameter_value(&query, "type"), + encryption: get_parameter_value(&query, "encryption"), + header_type: get_parameter_value(&query, "headerType"), + host: url_decode(get_parameter_value(&query, "host")), + seed: url_decode(get_parameter_value(&query, "seed")), + quic_security: get_parameter_value(&query, "quicSecurity"), + key: get_parameter_value(&query, "key"), + mode: url_decode(get_parameter_value(&query, "mode")), + service_name: url_decode(get_parameter_value(&query, "serviceName")), + slpn: get_parameter_value(&query, "slpn"), + spx: url_decode(get_parameter_value(&query, "spx")), + extra: url_decode(get_parameter_value(&query, "extra")), + allowInsecure: get_parameter_value(&query, "allowInsecure"), }; } @@ -64,121 +89,3 @@ fn parse_vless_query(raw_query: &str) -> models::VlessQuery { }; return a; } - -#[cfg(test)] -mod tests { - use crate::parser::vless::create_outbound_object; - - use super::*; - - #[test] - fn vless_tcp_header_test() { - let v = "vless://1010501a-ca9a-479c-84d0-1308d97789b5@104.21.25.109:443?security=reality&alpn=http%2F1.1&encryption=none&pbk=testpub&host=mehr14-n.gowow31220.workers.dev&headerType=http&fp=chrome&spx=spideex&type=tcp&flow=xtls-rprx-vision&sni=mehr14-iran-mehr14-iran-mehr14-iran-mehr14-iran-mehr14-iran.gowow31220.workers.dev&sid=testshort#%E2%AD%90%EF%B8%8F%20Telegram%20%3D%20%40z_v2ray"; - let data = create_outbound_object(get_data(v)); - assert_eq!( - data.streamSettings - .tcpSettings - .unwrap() - .header - .unwrap() - .r#type, - Some(String::from("http")) - ) - } - - #[test] - fn vless_test_2() { - let v = "vless://2dc56709-sdfs-sdfs-2234-128904@nwarne.fast-ip.com:80/?type=ws&encryption=none&host=Shuposipet.com&path=%2Fde%3Fed%3D1048#[test]@test"; - let data = get_data(v); - assert_eq!(data.address_data.address, "nwarne.fast-ip.com"); - assert_eq!(data.address_data.uuid, "2dc56709-sdfs-sdfs-2234-128904"); - assert_eq!(data.address_data.port, 80); - assert_eq!(data.query.encryption.unwrap(), "none"); - assert_eq!(data.query.r#type.unwrap(), "ws"); - } - - #[test] - fn vless_test() { - let v = "vless://4d2c3e35-749d-52e3-bdb6-3f3f4950c183@tre.test.one:2053?security=reality&type=tcp&flow=xtls-rprx-vision#test-name"; - let data = get_data(v); - assert_eq!(data.address_data.address, "tre.test.one"); - assert_eq!( - data.address_data.uuid, - "4d2c3e35-749d-52e3-bdb6-3f3f4950c183" - ); - assert_eq!(data.address_data.port, 2053); - assert_eq!(data.query.flow.unwrap(), "xtls-rprx-vision"); - assert_eq!(data.query.security.unwrap(), "reality"); - assert_eq!(data.query.r#type.unwrap(), "tcp"); - } - - #[test] - fn parse_vless_query_data() { - let query = "security=reality&sni=bench.sh&fp=chrome&pbk=7xhH4b_VkliBxGulljcyPOH-bYUA2dl-XAdZAsfhk04&sid=6ba85179e30d4fc2&type=tcp&flow=xtls-rprx-vision&alpn=http%2F1.1&path=/"; - let parsed_query = parse_vless_query(query); - assert_eq!(parsed_query.sni.unwrap(), "bench.sh"); - assert_eq!(parsed_query.security.unwrap(), "reality"); - assert_eq!(parsed_query.fp.unwrap(), "chrome"); - assert_eq!( - parsed_query.pbk.unwrap(), - "7xhH4b_VkliBxGulljcyPOH-bYUA2dl-XAdZAsfhk04" - ); - assert_eq!(parsed_query.sid.unwrap(), "6ba85179e30d4fc2"); - assert_eq!(parsed_query.r#type.unwrap(), "tcp"); - assert_eq!(parsed_query.flow.unwrap(), "xtls-rprx-vision"); - assert_eq!(parsed_query.alpn.unwrap(), "http/1.1"); - assert_eq!(parsed_query.path.unwrap(), "/"); - assert_eq!(parsed_query.encryption, None); - assert_eq!(parsed_query.header_type, None); - assert_eq!(parsed_query.host, None); - assert_eq!(parsed_query.seed, None); - assert_eq!(parsed_query.quic_security, None); - assert_eq!(parsed_query.key, None); - assert_eq!(parsed_query.mode, None); - assert_eq!(parsed_query.service_name, None); - assert_eq!(parsed_query.slpn, None); - assert_eq!(parsed_query.spx, None); - } - - #[test] - fn parse_vless_query_with_defaults() { - let query = ""; - let parsed_query = parse_vless_query(query); - assert_eq!(parsed_query.sni, None); - assert_eq!(parsed_query.security, None); - assert_eq!(parsed_query.fp, None); - assert_eq!(parsed_query.pbk, None); - assert_eq!(parsed_query.sid, None); - assert_eq!(parsed_query.r#type, None); - assert_eq!(parsed_query.r#flow, None); - assert_eq!(parsed_query.path, None); - assert_eq!(parsed_query.encryption, None); - assert_eq!(parsed_query.header_type, None); - assert_eq!(parsed_query.host, None); - assert_eq!(parsed_query.seed, None); - assert_eq!(parsed_query.quic_security, None); - assert_eq!(parsed_query.key, None); - assert_eq!(parsed_query.mode, None); - assert_eq!(parsed_query.service_name, None); - assert_eq!(parsed_query.slpn, None); - assert_eq!(parsed_query.spx, None); - } - - #[test] - fn parse_vless_host() { - let raw_host = "uu0id@127.0.0.1:3012"; - let parsed = parse_vless_address(raw_host); - assert_eq!(parsed.address, "127.0.0.1"); - assert_eq!(parsed.port, 3012); - assert_eq!(parsed.uuid, "uu0id"); - } - - #[test] - fn parse_vless_ipv6_host() { - let raw_host = "uu0id@[2a06:98c1:3120::1]:443"; - let parsed = parse_vless_address(raw_host); - assert_eq!(parsed.port, 443); - assert_eq!(parsed.address, "[2a06:98c1:3120::1]"); - assert_eq!(parsed.uuid, "uu0id"); - } -} diff --git a/src/parser/vless/mod.rs b/src/parser/vless/mod.rs index 3dcb4ba..aee3620 100644 --- a/src/parser/vless/mod.rs +++ b/src/parser/vless/mod.rs @@ -2,121 +2,17 @@ pub mod data; mod models; use crate::{config_models::*, utils::parse_raw_json}; -pub fn create_outbound_object(data: models::VlessData) -> Outbound { - let network_type = data.query.r#type.clone().unwrap_or(String::from("")); - - let allow_insecure = data.query.allowInsecure == Some(String::from("true")) - || data.query.allowInsecure == Some(String::from("1")); - - return Outbound { - protocol: String::from("vless"), - tag: String::from("proxy"), - streamSettings: StreamSettings { - network: data.query.r#type.clone(), - security: data.query.security.clone(), - tlsSettings: if data.query.security == Some(String::from("tls")) { - Some(TlsSettings { - alpn: data.query.alpn.map(|alpn| vec![alpn]), - rejectUnknownSni: None, - enableSessionResumption: None, - minVersion: None, - maxVersion: None, - cipherSuites: None, - disableSystemRoot: None, - preferServerCipherSuites: None, - fingerprint: data.query.fp.clone(), - serverName: data.query.sni.clone(), - allowInsecure: allow_insecure, - }) - } else { - None - }, - wsSettings: if network_type == String::from("ws") { - Some(WsSettings { - Host: data.query.host.clone(), - path: data.query.path.clone(), - acceptProxyProtocol: None, - }) - } else { - None - }, - tcpSettings: if network_type == String::from("tcp") { - Some(TCPSettings { - header: Some(TCPHeader { - r#type: Some(data.query.header_type.unwrap_or(String::from("none"))), - }), - acceptProxyProtocol: None, - }) - } else { - None - }, - realitySettings: if network_type == String::from("reality") { - Some(RealitySettings { - publicKey: data.query.pbk, - serverName: data.query.sni.clone(), - shortId: data.query.sid, - spiderX: Some(String::from("")), - fingerprint: data.query.fp.clone(), - }) - } else { - None - }, - grpcSettings: if network_type == String::from("grpc") { - Some(GRPCSettings { - authority: data.query.authority, - multiMode: Some(false), - serviceName: data.query.service_name, - }) - } else { - None - }, - quicSettings: if network_type == String::from("quic") { - Some(QuicSettings { - header: Some(NonHeaderObject { - r#type: Some(String::from("none")), - }), - security: Some(String::from("none")), - key: Some(String::from("")), - }) - } else { - None - }, - kcpSettings: if network_type == String::from("kcp") { - Some(KCPSettings { - mtu: None, - tti: None, - congestion: None, - uplinkCapacity: None, - readBufferSize: None, - writeBufferSize: None, - downlinkCapacity: None, - seed: data.query.seed, - }) - } else { - None - }, - xhttpSettings: if network_type == String::from("xhttp") { - Some(XHTTPSettings { - host: data.query.host.clone(), - path: data.query.path.clone(), - mode: data.query.mode, - extra: data.query.extra.and_then(|e| parse_raw_json(e.as_str())), - }) - } else { - None - }, - }, - settings: OutboundSettings::Vless(VlessOutboundSettings { - vnext: vec![VlessServerObject { - port: data.address_data.port, - address: data.address_data.address, - users: vec![VlessUser { - id: data.address_data.uuid, - flow: data.query.flow, - encryption: data.query.encryption.unwrap_or(String::from("none")), - level: Some(0), - }], - }], - }), - }; +pub fn create_outbound_settings(data: &RawData) -> OutboundSettings { + return OutboundSettings::Vless(VlessOutboundSettings { + vnext: vec![VlessServerObject { + port: data.port, + address: data.address.clone(), + users: Some(vec![VlessUser { + id: data.uuid.clone(), + flow: data.flow.clone(), + encryption: Some(data.encryption.clone().unwrap_or(String::from("none"))), + level: Some(0), + }]), + }], + }); } From 82be554317af2fe40fa1a5302db414dc8e408500 Mon Sep 17 00:00:00 2001 From: Keivan-sf Date: Sat, 26 Jul 2025 21:00:26 +0330 Subject: [PATCH 2/4] Remove unused structs --- src/parser/vless/models.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/parser/vless/models.rs b/src/parser/vless/models.rs index 363927f..cd8db5e 100644 --- a/src/parser/vless/models.rs +++ b/src/parser/vless/models.rs @@ -1,36 +1,5 @@ -#[allow(non_snake_case)] -pub struct VlessQuery { - pub security: Option, - pub sni: Option, - pub fp: Option, - pub pbk: Option, - pub sid: Option, - pub r#type: Option, - pub flow: Option, - pub path: Option, - pub encryption: Option, - pub header_type: Option, - pub host: Option, - pub seed: Option, - pub quic_security: Option, - pub r#key: Option, - pub mode: Option, - pub service_name: Option, - pub authority: Option, - pub slpn: Option, - pub spx: Option, - pub alpn: Option, - pub extra: Option, - pub allowInsecure: Option, -} - pub struct VlessAddress { pub uuid: String, pub address: String, pub port: u16, } - -pub struct VlessData { - pub query: VlessQuery, - pub address_data: VlessAddress, -} From cfe3c2ed531890cc5a6282e77bcc2d4a4b2ab356 Mon Sep 17 00:00:00 2001 From: Keivan-sf Date: Sat, 26 Jul 2025 22:19:02 +0330 Subject: [PATCH 3/4] Parse vmess when not shared via base64 --- src/config_models/mod.rs | 6 ++++ src/parser/mod.rs | 6 ++++ src/parser/vless/data.rs | 30 ------------------- src/parser/vmess/data.rs | 61 ++++++++++++++++++++++++++++++++++++++ src/parser/vmess/mod.rs | 18 +++++++++++ src/parser/vmess/models.rs | 5 ++++ 6 files changed, 96 insertions(+), 30 deletions(-) create mode 100644 src/parser/vmess/data.rs create mode 100644 src/parser/vmess/mod.rs create mode 100644 src/parser/vmess/models.rs diff --git a/src/config_models/mod.rs b/src/config_models/mod.rs index c6f7b49..62f4012 100644 --- a/src/config_models/mod.rs +++ b/src/config_models/mod.rs @@ -20,10 +20,16 @@ pub struct VlessOutboundSettings { pub vnext: Vec, } +#[derive(Serialize, Deserialize)] +pub struct VmessOutboundSettings { + pub vnext: Vec, +} + #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum OutboundSettings { Vless(VlessOutboundSettings), + Vmess(VmessOutboundSettings), } #[derive(Serialize, Deserialize)] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a44ed2e..6675b37 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7,6 +7,7 @@ use crate::utils::{inbound_generator, parse_raw_json}; mod uri_identifier; mod vless; +mod vmess; pub fn create_json_config(uri: &str, socks_port: Option, http_port: Option) -> String { let config = create_config(uri, socks_port, http_port); @@ -40,6 +41,11 @@ pub fn create_outbound_object(uri: &str) -> config_models::Outbound { let s = vless::create_outbound_settings(&d); (String::from("vless"), d, s) } + Some(uri_identifier::Protocols::Vmess) => { + let d = vmess::data::get_data(uri); + let s = vmess::create_outbound_settings(&d); + (String::from("vmess"), d, s) + } Some(_) => { panic!("The protocol was recognized but is not supported yet"); } diff --git a/src/parser/vless/data.rs b/src/parser/vless/data.rs index b39c99d..5c71ba1 100644 --- a/src/parser/vless/data.rs +++ b/src/parser/vless/data.rs @@ -59,33 +59,3 @@ fn parse_vless_address(raw_data: &str) -> models::VlessAddress { port: parsed.port().unwrap().as_u16(), }; } - -fn parse_vless_query(raw_query: &str) -> models::VlessQuery { - let query: Vec<(&str, &str)> = querystring::querify(raw_query); - - let a = models::VlessQuery { - alpn: url_decode(get_parameter_value(&query, "alpn")), - path: url_decode(get_parameter_value(&query, "path")), - authority: url_decode(get_parameter_value(&query, "authority")), - pbk: url_decode(get_parameter_value(&query, "pbk")), - security: get_parameter_value(&query, "security"), - sid: url_decode(get_parameter_value(&query, "sid")), - flow: get_parameter_value(&query, "flow"), - sni: get_parameter_value(&query, "sni"), - fp: url_decode(get_parameter_value(&query, "fp")), - r#type: get_parameter_value(&query, "type"), - encryption: get_parameter_value(&query, "encryption"), - header_type: get_parameter_value(&query, "headerType"), - host: url_decode(get_parameter_value(&query, "host")), - seed: url_decode(get_parameter_value(&query, "seed")), - quic_security: get_parameter_value(&query, "quicSecurity"), - key: get_parameter_value(&query, "key"), - mode: url_decode(get_parameter_value(&query, "mode")), - service_name: url_decode(get_parameter_value(&query, "serviceName")), - slpn: get_parameter_value(&query, "slpn"), - spx: url_decode(get_parameter_value(&query, "spx")), - extra: url_decode(get_parameter_value(&query, "extra")), - allowInsecure: get_parameter_value(&query, "allowInsecure"), - }; - return a; -} diff --git a/src/parser/vmess/data.rs b/src/parser/vmess/data.rs new file mode 100644 index 0000000..7314ca7 --- /dev/null +++ b/src/parser/vmess/data.rs @@ -0,0 +1,61 @@ +use crate::config_models::RawData; +use crate::parser::vmess::models::{self, VmessAddress}; +use crate::utils::{get_parameter_value, url_decode}; +use http::Uri; + +pub fn get_data(uri: &str) -> RawData { + let data = uri.split_once("vmess://").unwrap().1; + let query_and_name = uri.split_once("?").unwrap().1; + let raw_query = query_and_name + .split_once("#") + .unwrap_or((query_and_name, "")) + .0; + let parsed_address = parse_vmess_address(data.split_once("?").unwrap().0); + let query: Vec<(&str, &str)> = querystring::querify(raw_query); + + return RawData { + uuid: Some(parsed_address.uuid), + port: Some(parsed_address.port), + address: Some(parsed_address.address), + alpn: url_decode(get_parameter_value(&query, "alpn")), + path: url_decode(get_parameter_value(&query, "path")), + authority: url_decode(get_parameter_value(&query, "authority")), + pbk: url_decode(get_parameter_value(&query, "pbk")), + security: get_parameter_value(&query, "security"), + sid: url_decode(get_parameter_value(&query, "sid")), + flow: get_parameter_value(&query, "flow"), + sni: get_parameter_value(&query, "sni"), + fp: url_decode(get_parameter_value(&query, "fp")), + r#type: get_parameter_value(&query, "type"), + encryption: get_parameter_value(&query, "encryption"), + header_type: get_parameter_value(&query, "headerType"), + host: url_decode(get_parameter_value(&query, "host")), + seed: url_decode(get_parameter_value(&query, "seed")), + quic_security: get_parameter_value(&query, "quicSecurity"), + key: get_parameter_value(&query, "key"), + mode: url_decode(get_parameter_value(&query, "mode")), + service_name: url_decode(get_parameter_value(&query, "serviceName")), + slpn: get_parameter_value(&query, "slpn"), + spx: url_decode(get_parameter_value(&query, "spx")), + extra: url_decode(get_parameter_value(&query, "extra")), + allowInsecure: get_parameter_value(&query, "allowInsecure"), + }; +} + +fn parse_vmess_address(raw_data: &str) -> VmessAddress { + let (uuid, raw_address): (String, &str) = match raw_data.split_once("@") { + None => { + panic!("Wrong vmess format, no `@` found in the address"); + } + Some(data) => (String::from(data.0), data.1), + }; + let address_wo_slash = raw_address.strip_suffix("/").unwrap_or(raw_address); + + let parsed = address_wo_slash.parse::().unwrap(); + + return VmessAddress { + uuid: url_decode(Some(uuid)).unwrap(), + address: parsed.host().unwrap().to_string(), + port: parsed.port().unwrap().as_u16(), + }; +} diff --git a/src/parser/vmess/mod.rs b/src/parser/vmess/mod.rs new file mode 100644 index 0000000..94f30fe --- /dev/null +++ b/src/parser/vmess/mod.rs @@ -0,0 +1,18 @@ +pub mod data; +mod models; +use crate::{config_models::*, utils::parse_raw_json}; + +pub fn create_outbound_settings(data: &RawData) -> OutboundSettings { + return OutboundSettings::Vmess(VmessOutboundSettings { + vnext: vec![VlessServerObject { + port: data.port, + address: data.address.clone(), + users: Some(vec![VlessUser { + id: data.uuid.clone(), + flow: data.flow.clone(), + encryption: Some(data.encryption.clone().unwrap_or(String::from("none"))), + level: Some(0), + }]), + }], + }); +} diff --git a/src/parser/vmess/models.rs b/src/parser/vmess/models.rs new file mode 100644 index 0000000..6cf317b --- /dev/null +++ b/src/parser/vmess/models.rs @@ -0,0 +1,5 @@ +pub struct VmessAddress { + pub uuid: String, + pub address: String, + pub port: u16, +} From c5ce2b56eee977aab285b295ecb73a655000921c Mon Sep 17 00:00:00 2001 From: Keivan-sf Date: Sun, 27 Jul 2025 12:21:49 +0330 Subject: [PATCH 4/4] Parse base64 vmess uri --- Cargo.lock | 7 +++++ Cargo.toml | 1 + src/config_models/mod.rs | 2 ++ src/parser/vless/data.rs | 1 + src/parser/vless/mod.rs | 1 + src/parser/vmess/data.rs | 62 +++++++++++++++++++++++++++++++++++++++- src/parser/vmess/mod.rs | 1 + 7 files changed, 74 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 334cadf..edfa879 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bytes" version = "1.10.1" @@ -275,6 +281,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" name = "v2parser" version = "0.1.1" dependencies = [ + "base64", "clap", "http", "querystring", diff --git a/Cargo.toml b/Cargo.toml index b2a35bc..1247e7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +base64 = "0.22.1" clap = { version = "4.4.6", features = ["derive"] } http = "1.3.1" querystring = "1.1.0" diff --git a/src/config_models/mod.rs b/src/config_models/mod.rs index 62f4012..d277953 100644 --- a/src/config_models/mod.rs +++ b/src/config_models/mod.rs @@ -6,6 +6,7 @@ pub struct VlessUser { pub encryption: Option, pub flow: Option, pub level: Option, + pub security: Option, } #[derive(Serialize, Deserialize)] @@ -186,6 +187,7 @@ pub struct Config { #[allow(non_snake_case)] pub struct RawData { pub security: Option, + pub vnext_security: Option, pub sni: Option, pub fp: Option, pub pbk: Option, diff --git a/src/parser/vless/data.rs b/src/parser/vless/data.rs index 5c71ba1..42b01ee 100644 --- a/src/parser/vless/data.rs +++ b/src/parser/vless/data.rs @@ -35,6 +35,7 @@ pub fn get_data(uri: &str) -> RawData { key: get_parameter_value(&query, "key"), mode: url_decode(get_parameter_value(&query, "mode")), service_name: url_decode(get_parameter_value(&query, "serviceName")), + vnext_security: None, slpn: get_parameter_value(&query, "slpn"), spx: url_decode(get_parameter_value(&query, "spx")), extra: url_decode(get_parameter_value(&query, "extra")), diff --git a/src/parser/vless/mod.rs b/src/parser/vless/mod.rs index aee3620..0ec40a1 100644 --- a/src/parser/vless/mod.rs +++ b/src/parser/vless/mod.rs @@ -12,6 +12,7 @@ pub fn create_outbound_settings(data: &RawData) -> OutboundSettings { flow: data.flow.clone(), encryption: Some(data.encryption.clone().unwrap_or(String::from("none"))), level: Some(0), + security: None, }]), }], }); diff --git a/src/parser/vmess/data.rs b/src/parser/vmess/data.rs index 7314ca7..1877f14 100644 --- a/src/parser/vmess/data.rs +++ b/src/parser/vmess/data.rs @@ -1,11 +1,70 @@ use crate::config_models::RawData; use crate::parser::vmess::models::{self, VmessAddress}; use crate::utils::{get_parameter_value, url_decode}; +use base64::{engine::general_purpose, Engine}; use http::Uri; +use serde_json::Value; pub fn get_data(uri: &str) -> RawData { let data = uri.split_once("vmess://").unwrap().1; + + return match general_purpose::STANDARD.decode(data) { + Ok(decoded) => get_raw_data_from_base64(&decoded), + Err(_) => get_raw_data_from_uri(data), + }; +} + +fn get_raw_data_from_base64(decoded_base64: &Vec) -> RawData { + let json_str = std::str::from_utf8(decoded_base64).unwrap(); + let json = serde_json::from_str::(json_str).unwrap(); + + return RawData { + uuid: get_str_field(&json, "id"), + port: get_str_field(&json, "port") + .and_then(|s| Some(s.parse::().expect("port is not a number"))), + address: get_str_field(&json, "add"), + alpn: url_decode(get_str_field(&json, "alpn")), + path: url_decode(get_str_field(&json, "path")), + authority: url_decode(get_str_field(&json, "host")), + // this probably does not exist in vmess uri + pbk: url_decode(get_str_field(&json, "pbk")), + security: get_str_field(&json, "tls"), + vnext_security: get_str_field(&json, "scy"), + // this probably does not exist in vmess uri + sid: url_decode(get_str_field(&json, "pbk")), + // this probably does not exist in vmess uri + flow: url_decode(get_str_field(&json, "flow")), + sni: get_str_field(&json, "sni"), + fp: url_decode(get_str_field(&json, "fp")), + r#type: url_decode(get_str_field(&json, "net")), + encryption: None, + header_type: url_decode(get_str_field(&json, "type")), + host: url_decode(get_str_field(&json, "host")), + // this probably does not exist in vmess uri + seed: url_decode(get_str_field(&json, "seed")), + quic_security: None, + key: None, + mode: url_decode(get_str_field(&json, "type")), + service_name: url_decode(get_str_field(&json, "path")), + // this probably does not exist in vmess uri + slpn: url_decode(get_str_field(&json, "slpn")), + // this probably does not exist in vmess uri + spx: url_decode(get_str_field(&json, "spx")), + // this probably does not exist in vmess uri + extra: url_decode(get_str_field(&json, "extra")), + // this probably does not exist in vmess uri + allowInsecure: None, + }; +} + +fn get_str_field(json: &Value, field: &str) -> Option { + return json.get(field).and_then(|v| v.as_str()).map(String::from); +} + +fn get_raw_data_from_uri(uri: &str) -> RawData { + let data = uri.split_once("vmess://").unwrap().1; let query_and_name = uri.split_once("?").unwrap().1; + let raw_query = query_and_name .split_once("#") .unwrap_or((query_and_name, "")) @@ -35,6 +94,7 @@ pub fn get_data(uri: &str) -> RawData { key: get_parameter_value(&query, "key"), mode: url_decode(get_parameter_value(&query, "mode")), service_name: url_decode(get_parameter_value(&query, "serviceName")), + vnext_security: None, slpn: get_parameter_value(&query, "slpn"), spx: url_decode(get_parameter_value(&query, "spx")), extra: url_decode(get_parameter_value(&query, "extra")), @@ -45,7 +105,7 @@ pub fn get_data(uri: &str) -> RawData { fn parse_vmess_address(raw_data: &str) -> VmessAddress { let (uuid, raw_address): (String, &str) = match raw_data.split_once("@") { None => { - panic!("Wrong vmess format, no `@` found in the address"); + panic!("Wrong vmess format, no `@` found in the address and it was not a valid base64"); } Some(data) => (String::from(data.0), data.1), }; diff --git a/src/parser/vmess/mod.rs b/src/parser/vmess/mod.rs index 94f30fe..36b7497 100644 --- a/src/parser/vmess/mod.rs +++ b/src/parser/vmess/mod.rs @@ -12,6 +12,7 @@ pub fn create_outbound_settings(data: &RawData) -> OutboundSettings { flow: data.flow.clone(), encryption: Some(data.encryption.clone().unwrap_or(String::from("none"))), level: Some(0), + security: data.vnext_security.clone(), }]), }], });