Skip to content

Commit ec4ecf2

Browse files
committed
hysteria2 hop
1 parent d22ff7c commit ec4ecf2

File tree

9 files changed

+341
-433
lines changed

9 files changed

+341
-433
lines changed

Cargo.lock

Lines changed: 56 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clash/tests/data/config/hysteria2.yaml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ port: 8891
33
socks-port: 8889
44
mixed-port: 8888
55

6-
76
dns:
87
enable: true
98
listen: 127.0.0.1:53533
@@ -50,8 +49,19 @@ proxies:
5049
password: passwd
5150
sni: example.com
5251
skip-cert-verify: true
53-
obfs: salamander
54-
obfs-password: "passwd"
52+
53+
# fingerprint: "DF:F8:47:B5:6A:A3:8D:E8:A8:09:76:70:DD:1D:BB:DF:EB:3D:FB:07:FB:12:EE:C5:A9:BC:9B:07:26:7A:75:33"
54+
# or
55+
# fingerprint: "dff847b56aa38de8a8097670dd1dbbdfeb3dfb07fb12eec5a9bc9b07267a7533"
56+
57+
# obfs: salamander
58+
# obfs-password: "passwd"
59+
60+
# server port hop range
61+
# if hysteria2 server is deployed on `localhost` for testing, you can use this iptables commands to redirect the traffic, otherwise https://v2.hysteria.network/docs/advanced/Port-Hopping/#server
62+
# sudo iptables -t nat -I PREROUTING -p udp --dport 15000:16000 -j REDIRECT --to-ports 10086
63+
# sudo iptables -t nat -I OUTPUT -o lo -p udp --dport 15000:16000 -j REDIRECT --to-ports 10086
64+
ports: "15000-16000"
5565

5666
rules:
5767
- MATCH, local

clash_lib/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ tokio-test = "0.4.4"
146146
axum-macros = "0.4.2"
147147
bollard = "0.17"
148148
serial_test = "3.1"
149-
env_logger = "0.11"
149+
env_logger = "*"
150150

151151
[build-dependencies]
152152
prost-build = "0.13"

clash_lib/src/app/profile/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{collections::HashMap, sync::Arc};
22

33
use serde::{Deserialize, Serialize};
4-
use tracing::{error, trace, warn};
4+
use tracing::{error, warn};
55

66
#[derive(Serialize, Deserialize, Debug, Clone)]
77
struct Db {

clash_lib/src/config/internal/proxy.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use crate::{common::utils::default_bool_true, config::utils, Error};
1+
use crate::{common::utils::{decode_hex, default_bool_true}, config::utils, Error};
22
use serde::{de::value::MapDeserializer, Deserialize};
33
use serde_yaml::Value;
44
use std::{
55
collections::HashMap,
6-
fmt::{Display, Formatter},
6+
fmt::{Display, Formatter}, path::PathBuf,
77
};
88
use uuid::Uuid;
99

@@ -499,8 +499,11 @@ pub struct OutboundHysteria2 {
499499
pub sni: Option<String>,
500500
pub skip_cert_verify: bool,
501501
pub ca: Option<String>,
502-
pub ca_str: Option<String>,
503-
pub fingerprint: Option<String>,
502+
pub ca_str: Option<PathBuf>,
503+
// a hex_encoded sha256 fingerprint of the server certificate
504+
#[serde(deserialize_with = "deserialize_sha256")]
505+
#[serde(default)]
506+
pub fingerprint: Option<[u8; 32]>,
504507
/// bbr congestion control window
505508
pub cwnd: Option<u64>,
506509
}
@@ -510,3 +513,23 @@ pub struct OutboundHysteria2 {
510513
pub enum Hysteria2Obfs {
511514
Salamander,
512515
}
516+
517+
fn deserialize_sha256<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
518+
where
519+
D: serde::Deserializer<'de>,
520+
{
521+
let s = Option::<String>::deserialize(deserializer)?.map(|x| x.replace(':', ""));
522+
if let Some(s) = s {
523+
let s = decode_hex(&s).map_err(serde::de::Error::custom)?;
524+
if s.len() != 32 {
525+
return Err(serde::de::Error::custom(
526+
"fingerprint must be 32 bytes long",
527+
));
528+
}
529+
let mut arr = [0; 32];
530+
arr.copy_from_slice(&s);
531+
Ok(Some(arr))
532+
} else {
533+
Ok(None)
534+
}
535+
}

clash_lib/src/proxy/converters/hysteria2.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::{
22
num::{NonZeroU16, ParseIntError},
33
ops::RangeInclusive,
4+
path::PathBuf,
45
sync::Arc,
56
};
67

78
use rand::Rng;
9+
use rustls::pki_types::{pem::PemObject, CertificateDer};
810

911
use crate::{
1012
config::internal::proxy::{Hysteria2Obfs, OutboundHysteria2},
@@ -40,10 +42,8 @@ impl PortGenrateor {
4042
}
4143

4244
pub fn get(&self) -> u16 {
43-
let mut rng = rand::thread_rng();
44-
let len =
45-
1 + self.ports.len() + self.range.iter().map(|r| r.len()).sum::<usize>();
46-
let idx = rng.gen_range(0..len);
45+
let len = 1 /* default_port */ + self.ports.len() + self.range.iter().map(|r| r.len()).sum::<usize>();
46+
let idx = rand::thread_rng().gen_range(0..len);
4747
match idx {
4848
0 => self.default,
4949
idx if idx <= self.ports.len() => self.ports[idx - 1],
@@ -122,14 +122,13 @@ impl TryFrom<OutboundHysteria2> for AnyOutboundHandler {
122122
sni: value.sni.or(addr.domain().map(|s| s.to_owned())),
123123
addr,
124124
alpn: value.alpn.unwrap_or_default(),
125-
ca: value.ca.map(|s| s.into()),
125+
ca: parse_certificate(value.ca.as_deref(), value.ca_str.as_ref())?,
126126
fingerprint: value.fingerprint,
127127
skip_cert_verify: value.skip_cert_verify,
128128
passwd: value.password,
129129
ports: ports_gen,
130130
obfs,
131131
up_down: value.up.zip(value.down),
132-
ca_str: value.ca_str,
133132
cwnd: value.cwnd,
134133
};
135134

@@ -138,6 +137,40 @@ impl TryFrom<OutboundHysteria2> for AnyOutboundHandler {
138137
}
139138
}
140139

140+
fn parse_certificate(
141+
ca_str: Option<&str>,
142+
ca_path: Option<&PathBuf>,
143+
) -> Result<Option<CertificateDer<'static>>, crate::Error> {
144+
let ca_from_str = ca_str
145+
.map(|s| CertificateDer::from_pem_slice(s.as_bytes()).map(|c| c.to_owned()))
146+
.transpose()
147+
.map_err(|e| {
148+
crate::Error::InvalidConfig(format!("parse ca from str error: {:?}", e))
149+
})?;
150+
151+
let ca_from_path = ca_path
152+
.map(|p| CertificateDer::from_pem_file(p).map(|c| c.to_owned()))
153+
.transpose()
154+
.map_err(|e| {
155+
crate::Error::InvalidConfig(format!("parse ca from path error: {:?}", e))
156+
})?;
157+
158+
match (ca_from_str, ca_from_path) {
159+
(Some(ca), None) => Ok(Some(ca)),
160+
(None, Some(ca)) => Ok(Some(ca)),
161+
(Some(a), Some(b)) => {
162+
if a == b {
163+
Ok(a.into())
164+
} else {
165+
Err(crate::Error::InvalidConfig(
166+
"ca from str and path not equal".to_owned(),
167+
))
168+
}
169+
}
170+
(None, None) => Ok(None),
171+
}
172+
}
173+
141174
#[test]
142175
fn test_port_gen() {
143176
let p = PortGenrateor::new(1000).parse_ports_str("").unwrap();

0 commit comments

Comments
 (0)