Skip to main content

vpn_link_serde/
vless.rs

1//! VLess protocol parser
2//!
3//! URI format (RFC 3986): `vless://<id>@<address>:<port>[?<query>][#<fragment>]`
4//!
5//! **Required**: `id` (user UUID), `address` (host or IP), `port` (1–65535).
6//!
7//! **Query parameters** (optional, `application/x-www-form-urlencoded`): `encryption`, `flow` (e.g. `xtls-rprx-vision`), `security` (none/tls/xtls/reality), `type` (tcp/ws/grpc/h2/httpupgrade), `host`, `path`, `sni`, `fp`, `pbk` (Reality public key), `sid` (Reality short ID), `seed`, `headerType`.
8//!
9//! **Fragment**: Decoded as remark; must be URL-encoded if it contains spaces or non-ASCII.
10//!
11//! ## Parsing rules
12//!
13//! 1. Prefix `vless://` is case-insensitive.
14//! 2. Main part must contain exactly one `@` and a `:` for port (`id@address:port`); otherwise `InvalidFormat`.
15//! 3. Port must parse as u16; otherwise `InvalidField`.
16
17use crate::ProtocolParser;
18use crate::constants::{error_msg, scheme};
19use crate::error::{ProtocolError, Result};
20use serde::{Deserialize, Serialize};
21
22/// VLess configuration structure
23///
24/// Represents a complete VLess protocol configuration with all supported parameters.
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26pub struct VLessConfig {
27    /// User ID (UUID)
28    pub id: String,
29    /// Server address
30    pub address: String,
31    /// Server port
32    pub port: u16,
33    /// Encryption method
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub encryption: Option<String>,
36    /// Flow control (for XTLS)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub flow: Option<String>,
39    /// Security type (tls, xtls, reality, none)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub security: Option<String>,
42    /// Network type (tcp, kcp, ws, h2, quic, grpc, multi)
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub r#type: Option<String>,
45    /// Host header
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub host: Option<String>,
48    /// Path (for ws/h2/grpc)
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub path: Option<String>,
51    /// SNI (Server Name Indication)
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub sni: Option<String>,
54    /// Fingerprint
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub fp: Option<String>,
57    /// Public key (for Reality)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub pbk: Option<String>,
60    /// Short ID (for Reality)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub sid: Option<String>,
63    /// Seed (for mKCP)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub seed: Option<String>,
66    /// Header type
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub header_type: Option<String>,
69    /// Remark/description
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub remark: Option<String>,
72}
73
74/// VLess protocol parser
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct VLess {
77    /// VLess configuration
78    pub config: VLessConfig,
79}
80
81impl ProtocolParser for VLess {
82    fn parse(link: &str) -> Result<Self> {
83        if !link.to_lowercase().starts_with(scheme::VLESS) {
84            return Err(ProtocolError::InvalidFormat(format!(
85                "{} {}",
86                error_msg::MUST_START_WITH,
87                scheme::VLESS
88            )));
89        }
90
91        let link_body = &link[scheme::VLESS.len()..];
92
93        // Split into parts: [userinfo@]host:port[?query][#fragment]
94        let (main_part, query_part, fragment) = {
95            let hash_pos = link_body.find('#');
96            let (before_hash, fragment) = if let Some(pos) = hash_pos {
97                (&link_body[..pos], Some(&link_body[pos + 1..]))
98            } else {
99                (link_body, None)
100            };
101
102            let query_pos = before_hash.find('?');
103            let (main, query) = if let Some(pos) = query_pos {
104                (&before_hash[..pos], Some(&before_hash[pos + 1..]))
105            } else {
106                (before_hash, None)
107            };
108
109            (main, query, fragment)
110        };
111
112        // Parse main part: id@host:port
113        let at_pos = main_part
114            .find('@')
115            .ok_or_else(|| ProtocolError::InvalidFormat(error_msg::MISSING_AT.to_string()))?;
116
117        let id = &main_part[..at_pos];
118        let host_port = &main_part[at_pos + 1..];
119
120        let colon_pos = host_port.find(':').ok_or_else(|| {
121            ProtocolError::InvalidFormat(error_msg::MISSING_COLON_HOST_PORT.to_string())
122        })?;
123
124        let address = &host_port[..colon_pos];
125        let port_str = &host_port[colon_pos + 1..];
126        let port: u16 = port_str.parse().map_err(|e| {
127            ProtocolError::InvalidField(format!("{}: {}", error_msg::INVALID_PORT, e))
128        })?;
129
130        // Parse query parameters
131        let mut config = VLessConfig {
132            id: id.to_string(),
133            address: address.to_string(),
134            port,
135            encryption: None,
136            flow: None,
137            security: None,
138            r#type: None,
139            host: None,
140            path: None,
141            sni: None,
142            fp: None,
143            pbk: None,
144            sid: None,
145            seed: None,
146            header_type: None,
147            remark: fragment.map(|s| urlencoding::decode(s).unwrap_or_default().to_string()),
148        };
149
150        if let Some(query) = query_part {
151            let params: std::collections::HashMap<String, String> =
152                url::form_urlencoded::parse(query.as_bytes())
153                    .into_owned()
154                    .collect();
155
156            config.encryption = params.get("encryption").cloned();
157            config.flow = params.get("flow").cloned();
158            config.security = params.get("security").cloned();
159            config.r#type = params.get("type").cloned();
160            config.host = params.get("host").cloned();
161            config.path = params.get("path").cloned();
162            config.sni = params.get("sni").cloned();
163            config.fp = params.get("fp").cloned();
164            config.pbk = params.get("pbk").cloned();
165            config.sid = params.get("sid").cloned();
166            config.seed = params.get("seed").cloned();
167            config.header_type = params.get("headerType").cloned();
168        }
169
170        Ok(VLess { config })
171    }
172
173    fn to_link(&self) -> Result<String> {
174        let mut parts = vec![format!(
175            "vless://{}@{}:{}",
176            self.config.id, self.config.address, self.config.port
177        )];
178
179        // Build query string
180        let mut query_params = Vec::new();
181
182        if let Some(ref encryption) = self.config.encryption {
183            query_params.push(format!("encryption={}", urlencoding::encode(encryption)));
184        }
185        if let Some(ref flow) = self.config.flow {
186            query_params.push(format!("flow={}", urlencoding::encode(flow)));
187        }
188        if let Some(ref security) = self.config.security {
189            query_params.push(format!("security={}", urlencoding::encode(security)));
190        }
191        if let Some(ref r#type) = self.config.r#type {
192            query_params.push(format!("type={}", urlencoding::encode(r#type)));
193        }
194        if let Some(ref host) = self.config.host {
195            query_params.push(format!("host={}", urlencoding::encode(host)));
196        }
197        if let Some(ref path) = self.config.path {
198            query_params.push(format!("path={}", urlencoding::encode(path)));
199        }
200        if let Some(ref sni) = self.config.sni {
201            query_params.push(format!("sni={}", urlencoding::encode(sni)));
202        }
203        if let Some(ref fp) = self.config.fp {
204            query_params.push(format!("fp={}", urlencoding::encode(fp)));
205        }
206        if let Some(ref pbk) = self.config.pbk {
207            query_params.push(format!("pbk={}", urlencoding::encode(pbk)));
208        }
209        if let Some(ref sid) = self.config.sid {
210            query_params.push(format!("sid={}", urlencoding::encode(sid)));
211        }
212        if let Some(ref seed) = self.config.seed {
213            query_params.push(format!("seed={}", urlencoding::encode(seed)));
214        }
215        if let Some(ref header_type) = self.config.header_type {
216            query_params.push(format!("headerType={}", urlencoding::encode(header_type)));
217        }
218
219        if !query_params.is_empty() {
220            parts.push(query_params.join("&"));
221        }
222
223        // Add fragment (remark)
224        if let Some(ref remark) = self.config.remark {
225            parts.push(format!("#{}", urlencoding::encode(remark)));
226        }
227
228        Ok(parts.join("?"))
229    }
230}