Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
/*.yaml
*.pem
.idea
1 change: 1 addition & 0 deletions src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ impl ResolvedLocation {
}

/// Get the port.
#[allow(dead_code)]
pub fn port(&self) -> u16 {
self.location.port()
}
Expand Down
122 changes: 122 additions & 0 deletions src/config/types/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,98 @@ pub enum ClientProxyConfig {
#[serde(default = "default_true", skip_serializing_if = "is_true")]
padding: bool,
},
#[serde(alias = "hy2")]
Hysteria2 {
password: String,
#[serde(default = "default_true")]
udp_enabled: bool,
#[serde(default)]
fast_open: bool,
/// Bandwidth configuration
#[serde(default)]
bandwidth: Option<Hysteria2Bandwidth>,
},
}

/// Bandwidth configuration for Hysteria2
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hysteria2Bandwidth {
/// Upload bandwidth (e.g., "100 mbps", "1 gbps")
pub up: Option<String>,
/// Download bandwidth (e.g., "200 mbps", "1 gbps")
pub down: Option<String>,
}

impl Hysteria2Bandwidth {
/// Parse upload bandwidth to bytes per second
pub fn parse_up(&self) -> std::io::Result<u64> {
self.parse_bandwidth(&self.up, "up")
}

/// Parse download bandwidth to bytes per second
pub fn parse_down(&self) -> std::io::Result<u64> {
self.parse_bandwidth(&self.down, "down")
}

fn parse_bandwidth(&self, value: &Option<String>, field: &str) -> std::io::Result<u64> {
let s = value.as_ref().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("bandwidth {} not specified", field),
)
})?;

let s = s.trim().to_lowercase();
// Find first non-digit, non-dot, non-space character to separate number from unit
let mut num_end = 0;
for (i, c) in s.chars().enumerate() {
if c.is_ascii_digit() || c == '.' {
num_end = i + 1;
} else if !c.is_whitespace() {
break;
}
}

let num_str = s[..num_end].trim();
let unit = s[num_end..].trim();

let num: f64 = num_str.parse().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid bandwidth value: {}", s),
)
})?;

let multiplier = match unit {
"bps" => 1.0,
"kbps" | "k" => 1024.0,
"mbps" | "m" => 1024.0 * 1024.0,
"gbps" | "g" => 1024.0 * 1024.0 * 1024.0,
"tbps" | "t" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
"" => 1024.0 * 1024.0, // Default to mbps if no unit
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown bandwidth unit: {}", unit),
));
}
};

Ok((num * multiplier / 8.0) as u64) // Convert bits to bytes
}
}

/// Resolve bandwidth config to actual bytes per second values
pub fn resolve_hysteria2_bandwidth(
bandwidth: &Option<Hysteria2Bandwidth>,
) -> std::io::Result<(u64, u64)> {
if let Some(bw) = bandwidth {
let up = bw.parse_up().unwrap_or(0);
let down = bw.parse_down().unwrap_or(0);
Ok((up, down))
} else {
Ok((0, 0))
}
}

impl ClientProxyConfig {
Expand All @@ -460,6 +552,7 @@ impl ClientProxyConfig {
ClientProxyConfig::PortForward => "PortForward",
ClientProxyConfig::Anytls { .. } => "AnyTLS",
ClientProxyConfig::Naiveproxy { .. } => "NaiveProxy",
ClientProxyConfig::Hysteria2 { .. } => "Hysteria2",
}
}
}
Expand Down Expand Up @@ -640,4 +733,33 @@ protocol:
assert!(result.is_ok());
assert!(matches!(result.unwrap(), ClientProxyConfig::Websocket(_)));
}

#[test]
fn test_client_proxy_config_hysteria2() {
let yaml = r#"
type: hysteria2
password: "test_password"
udp_enabled: true
"#;
let result: Result<ClientProxyConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_ok());
assert!(matches!(
result.unwrap(),
ClientProxyConfig::Hysteria2 { .. }
));
}

#[test]
fn test_client_proxy_config_hysteria2_alias() {
let yaml = r#"
type: hy2
password: "test_password"
"#;
let result: Result<ClientProxyConfig, _> = serde_yaml::from_str(yaml);
assert!(result.is_ok());
assert!(matches!(
result.unwrap(),
ClientProxyConfig::Hysteria2 { .. }
));
}
}
1 change: 1 addition & 0 deletions src/config/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod tun;
#[allow(unused_imports)]
pub use client::{
ClientConfig, ClientProxyConfig, H2MuxConfig, TlsClientConfig, WebsocketClientConfig,
Hysteria2Bandwidth, resolve_hysteria2_bandwidth,
};
pub use common::DEFAULT_REALITY_SHORT_ID;
pub use dns::{DnsConfig, DnsConfigGroup, DnsServerSpec, ExpandedDnsGroup, ExpandedDnsSpec};
Expand Down
10 changes: 10 additions & 0 deletions src/config/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,16 @@ fn validate_client_config(
));
}

// Hysteria2 must use QUIC transport
if matches!(client_config.protocol, ClientProxyConfig::Hysteria2 { .. })
&& client_config.transport != Transport::Quic
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Hysteria2 protocol requires transport: quic",
));
}

validate_client_proxy_config(&mut client_config.protocol, named_pems)?;

Ok(())
Expand Down
Loading