use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::address::NetLocation;
use crate::h2mux::{H2MuxOptions, MuxProtocol};
use crate::option_util::{NoneOrOne, NoneOrSome};
use super::common::{
default_reality_client_short_id, default_true, is_false, is_true, unspecified_address,
};
use super::server::WebsocketPingType;
use super::shadowsocks::ShadowsocksConfig;
use super::transport::{ClientQuicConfig, TcpConfig, Transport};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct H2MuxConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_connections: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_streams: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_streams: Option<u32>,
#[serde(default, skip_serializing_if = "is_false")]
pub padding: bool,
}
impl H2MuxConfig {
pub fn to_options(&self) -> H2MuxOptions {
H2MuxOptions {
protocol: MuxProtocol::H2Mux,
max_connections: self.max_connections.unwrap_or(4),
min_streams: self.min_streams.unwrap_or(4),
max_streams: self.max_streams.unwrap_or(0),
padding: self.padding,
}
}
}
fn deserialize_shadowsocks_client<'de, D>(
deserializer: D,
) -> Result<(ShadowsocksConfig, bool), D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ShadowsocksClientTemp {
cipher: String,
password: String,
#[serde(default = "default_true")]
udp_enabled: bool,
}
let temp = ShadowsocksClientTemp::deserialize(deserializer)?;
let config =
ShadowsocksConfig::from_fields(&temp.cipher, &temp.password).map_err(Error::custom)?;
Ok((config, temp.udp_enabled))
}
fn serialize_shadowsocks_client<S>(
config: &ShadowsocksConfig,
udp_enabled: &bool,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let field_count = if *udp_enabled { 2 } else { 3 };
let mut state = serializer.serialize_struct("Shadowsocks", field_count)?;
config.serialize_fields(&mut state)?;
if !*udp_enabled {
state.serialize_field("udp_enabled", udp_enabled)?;
}
state.end()
}
fn deserialize_snell_client<'de, D>(deserializer: D) -> Result<(ShadowsocksConfig, bool), D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SnellClientTemp {
cipher: String,
password: String,
#[serde(default = "default_true")]
udp_enabled: bool,
}
let temp = SnellClientTemp::deserialize(deserializer)?;
let config =
ShadowsocksConfig::from_fields(&temp.cipher, &temp.password).map_err(Error::custom)?;
Ok((config, temp.udp_enabled))
}
fn serialize_snell_client<S>(
config: &ShadowsocksConfig,
udp_enabled: &bool,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let field_count = if *udp_enabled { 2 } else { 3 };
let mut state = serializer.serialize_struct("Snell", field_count)?;
config.serialize_fields(&mut state)?;
if !*udp_enabled {
state.serialize_field("udp_enabled", udp_enabled)?;
}
state.end()
}
fn deserialize_vmess_client<'de, D>(
deserializer: D,
) -> Result<(String, String, bool, Option<H2MuxConfig>), D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct VmessClientTemp {
cipher: String,
user_id: String,
#[serde(default, alias = "force_aead")]
aead: Option<bool>,
#[serde(default = "default_true")]
udp_enabled: bool,
#[serde(default)]
h2mux: Option<H2MuxConfig>,
}
let temp = VmessClientTemp::deserialize(deserializer)?;
if let Some(aead_value) = temp.aead {
if !aead_value {
return Err(Error::custom(
"Non-AEAD VMess mode (aead=false or force_aead=false) is no longer supported. \
Please remove the aead/force_aead field from your configuration, or set it to true.",
));
}
log::warn!(
"The 'aead'/'force_aead' field in VMess client configuration is deprecated and will be removed in a future version. \
AEAD mode is now always enabled. Please remove this field from your configuration."
);
}
Ok((temp.cipher, temp.user_id, temp.udp_enabled, temp.h2mux))
}
fn deserialize_tls_client_config<'de, D>(deserializer: D) -> Result<TlsClientConfig, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TlsClientConfigTemp {
#[serde(default = "default_true")]
verify: bool,
#[serde(alias = "server_fingerprint", default)]
server_fingerprints: NoneOrSome<String>,
#[serde(default)]
sni_hostname: NoneOrOne<String>,
#[serde(alias = "alpn_protocol", default)]
alpn_protocols: NoneOrSome<String>,
#[serde(default)]
tls_buffer_size: Option<usize>,
#[serde(default)]
key: Option<String>,
#[serde(default)]
cert: Option<String>,
#[serde(default)]
shadowtls_password: Option<String>,
#[serde(default)]
vision: bool,
protocol: Box<ClientProxyConfig>,
}
let temp = TlsClientConfigTemp::deserialize(deserializer)?;
if temp.vision && temp.shadowtls_password.is_some() {
return Err(Error::custom(
"TLS client config cannot have both vision=true and shadowtls_password set. \
Vision and ShadowTLS are incompatible. \
Use either 'vision: true' with regular TLS, or 'type: shadowtls' for ShadowTLS.",
));
}
if let Some(password) = temp.shadowtls_password {
log::warn!(
"The 'shadowtls_password' field in TLS client configuration is deprecated. \
Please use 'type: shadowtls' with 'password' field instead. \
This field will be removed in a future version."
);
return Ok(TlsClientConfig {
verify: temp.verify,
server_fingerprints: temp.server_fingerprints,
sni_hostname: temp.sni_hostname.clone(),
alpn_protocols: temp.alpn_protocols,
tls_buffer_size: temp.tls_buffer_size,
key: temp.key,
cert: temp.cert,
vision: false,
protocol: Box::new(ClientProxyConfig::ShadowTls {
password,
sni_hostname: temp.sni_hostname.into_option(),
protocol: temp.protocol,
}),
});
}
Ok(TlsClientConfig {
verify: temp.verify,
server_fingerprints: temp.server_fingerprints,
sni_hostname: temp.sni_hostname,
alpn_protocols: temp.alpn_protocols,
tls_buffer_size: temp.tls_buffer_size,
key: temp.key,
cert: temp.cert,
vision: temp.vision,
protocol: temp.protocol,
})
}
fn deserialize_tls_variant<'de, D>(deserializer: D) -> Result<TlsClientConfig, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_tls_client_config(deserializer)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ClientConfig {
#[serde(default, skip_serializing_if = "NoneOrOne::is_unspecified")]
pub bind_interface: NoneOrOne<String>,
#[serde(
default = "unspecified_address",
skip_serializing_if = "NetLocation::is_unspecified"
)]
pub address: NetLocation,
pub protocol: ClientProxyConfig,
#[serde(default, skip_serializing_if = "Transport::is_default")]
pub transport: Transport,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tcp_settings: Option<TcpConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quic_settings: Option<ClientQuicConfig>,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
bind_interface: NoneOrOne::None,
address: unspecified_address(),
protocol: ClientProxyConfig::Direct,
transport: Transport::default(),
tcp_settings: None,
quic_settings: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ClientProxyConfig {
Direct,
Http {
#[serde(default, skip_serializing_if = "Option::is_none")]
username: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
password: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
resolve_hostname: bool,
},
#[serde(alias = "socks5")]
Socks {
#[serde(default, skip_serializing_if = "Option::is_none")]
username: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
password: Option<String>,
},
#[serde(
alias = "ss",
deserialize_with = "deserialize_shadowsocks_client",
serialize_with = "serialize_shadowsocks_client"
)]
Shadowsocks {
config: ShadowsocksConfig,
udp_enabled: bool,
},
#[serde(
deserialize_with = "deserialize_snell_client",
serialize_with = "serialize_snell_client"
)]
Snell {
config: ShadowsocksConfig,
udp_enabled: bool,
},
Vless {
user_id: String,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
udp_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
h2mux: Option<H2MuxConfig>,
},
Trojan {
password: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
shadowsocks: Option<ShadowsocksConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
h2mux: Option<H2MuxConfig>,
},
Reality {
public_key: String,
#[serde(default = "default_reality_client_short_id")]
short_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
sni_hostname: Option<String>,
#[serde(
alias = "cipher_suite",
default,
skip_serializing_if = "NoneOrSome::is_unspecified"
)]
cipher_suites: NoneOrSome<crate::reality::CipherSuite>,
#[serde(default, skip_serializing_if = "is_false")]
vision: bool,
protocol: Box<ClientProxyConfig>,
},
#[serde(alias = "shadowtls")]
ShadowTls {
password: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
sni_hostname: Option<String>,
protocol: Box<ClientProxyConfig>,
},
#[serde(deserialize_with = "deserialize_tls_variant")]
Tls(TlsClientConfig),
#[serde(deserialize_with = "deserialize_vmess_client")]
Vmess {
cipher: String,
user_id: String,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
udp_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
h2mux: Option<H2MuxConfig>,
},
#[serde(alias = "ws")]
Websocket(WebsocketClientConfig),
#[serde(alias = "noop")]
PortForward,
Anytls {
password: String,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
udp_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
padding_scheme: Option<Vec<String>>,
},
#[serde(alias = "naive")]
Naiveproxy {
username: String,
password: String,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
padding: bool,
},
}
impl ClientProxyConfig {
pub fn is_direct(&self) -> bool {
matches!(self, ClientProxyConfig::Direct)
}
pub fn protocol_name(&self) -> &str {
match self {
ClientProxyConfig::Direct => "Direct",
ClientProxyConfig::Http { .. } => "HTTP",
ClientProxyConfig::Socks { .. } => "SOCKS5",
ClientProxyConfig::Shadowsocks { .. } => "Shadowsocks",
ClientProxyConfig::Snell { .. } => "Snell",
ClientProxyConfig::Vless { .. } => "VLESS",
ClientProxyConfig::Trojan { .. } => "Trojan",
ClientProxyConfig::Reality { .. } => "Reality",
ClientProxyConfig::Tls(..) => "TLS",
ClientProxyConfig::ShadowTls { .. } => "ShadowTLS",
ClientProxyConfig::Vmess { .. } => "VMess",
ClientProxyConfig::Websocket(..) => "WebSocket",
ClientProxyConfig::PortForward => "PortForward",
ClientProxyConfig::Anytls { .. } => "AnyTLS",
ClientProxyConfig::Naiveproxy { .. } => "NaiveProxy",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TlsClientConfig {
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub verify: bool,
#[serde(
alias = "server_fingerprint",
default,
skip_serializing_if = "NoneOrSome::is_unspecified"
)]
pub server_fingerprints: NoneOrSome<String>,
#[serde(default, skip_serializing_if = "NoneOrOne::is_unspecified")]
pub sni_hostname: NoneOrOne<String>,
#[serde(
alias = "alpn_protocol",
default,
skip_serializing_if = "NoneOrSome::is_unspecified"
)]
pub alpn_protocols: NoneOrSome<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tls_buffer_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cert: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub vision: bool,
pub protocol: Box<ClientProxyConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebsocketClientConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub matching_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub matching_headers: Option<HashMap<String, String>>,
#[serde(default, skip_serializing_if = "WebsocketPingType::is_default")]
pub ping_type: WebsocketPingType,
pub protocol: Box<ClientProxyConfig>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
fn create_test_client_config() -> ClientConfig {
ClientConfig {
bind_interface: NoneOrOne::One("eth0".to_string()),
address: NetLocation::from_ip_addr(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 1080),
protocol: ClientProxyConfig::Socks {
username: Some("client_user".to_string()),
password: Some("client_pass".to_string()),
},
transport: Transport::Tcp,
tcp_settings: None,
quic_settings: None,
}
}
#[test]
fn test_client_config_serialization() {
let original = create_test_client_config();
let yaml_str = serde_yaml::to_string(&original).expect("Failed to serialize");
println!("Client config YAML:\n{yaml_str}");
let deserialized: ClientConfig =
serde_yaml::from_str(&yaml_str).expect("Failed to deserialize");
assert!(matches!(
deserialized.protocol,
ClientProxyConfig::Socks { .. }
));
}
#[test]
fn test_rejects_unknown_field_in_vmess_client() {
let yaml = r#"
type: vmess
cipher: aes-128-gcm
user_id: "b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4"
unknown_option: true
"#;
let result: Result<ClientProxyConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("unknown field `unknown_option`"),
"Error should mention unknown field: {err}"
);
}
#[test]
fn test_rejects_unknown_field_in_tls_client_config() {
let yaml = r#"
type: tls
verify: true
wrong_field: "oops"
protocol:
type: socks
"#;
let result: Result<ClientProxyConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("unknown field `wrong_field`"),
"Error should mention unknown field: {err}"
);
}
#[test]
fn test_rejects_unknown_field_in_client_config() {
let yaml = r#"
address: "127.0.0.1:9090"
protocol:
type: socks
invalid_client_field: "bad"
"#;
let result: Result<ClientConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("unknown field `invalid_client_field`"),
"Error should mention unknown field: {err}"
);
}
#[test]
fn test_client_proxy_config_direct() {
let yaml = r#"
type: direct
"#;
let result: Result<ClientProxyConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_ok());
assert!(result.unwrap().is_direct());
}
#[test]
fn test_client_proxy_config_socks() {
let yaml = r#"
type: socks
username: "user"
password: "pass"
"#;
let result: Result<ClientProxyConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_ok());
assert!(matches!(result.unwrap(), ClientProxyConfig::Socks { .. }));
}
#[test]
fn test_client_proxy_config_http() {
let yaml = r#"
type: http
"#;
let result: Result<ClientProxyConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_ok());
assert!(matches!(result.unwrap(), ClientProxyConfig::Http { .. }));
}
#[test]
fn test_websocket_client_config() {
let yaml = r#"
type: websocket
matching_path: "/ws"
protocol:
type: direct
"#;
let result: Result<ClientProxyConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_ok());
assert!(matches!(result.unwrap(), ClientProxyConfig::Websocket(_)));
}
}