Skip to content
Merged
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
17 changes: 15 additions & 2 deletions clash-lib/src/proxy/transport/reality/reality_client_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,15 +596,28 @@ impl RealityClientConnection {
record_len
);

// Decrypt using current sequence number
// Decrypt using current sequence number. When this fails at record 0,
// the peer most commonly forwarded camouflage/fallback TLS records
// because REALITY auth did not match; another possibility is a
// transcript/key-schedule mismatch between client and server.
let plaintext = decrypt_handshake_message(
cipher_suite,
&server_hs_key,
&server_hs_iv,
handshake_seq,
&ciphertext,
record_len as u16,
)?;
)
.map_err(|err| {
io::Error::new(
err.kind(),
format!(
"REALITY server handshake decryption failed at record #{handshake_seq} \
(record_len={record_len}, cipher_suite={cipher_suite:?}); \
possible fallback TLS data or mismatched public-key/short-id/servername: {err}"
),
)
})?;

log::debug!(
"REALITY CLIENT: Decrypted record #{} ({} bytes plaintext)",
Expand Down
119 changes: 101 additions & 18 deletions clash-lib/src/proxy/vless/vision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ pub struct VisionStream {
write_shutdown_queued: bool,
write_buf: BytesMut,
write_deframer: FuzzyTlsDeframer,
write_filter: VisionFilter,
read_deframer: FuzzyTlsDeframer,
traffic_filter: VisionFilter,
read_mode: ReadMode,
decoded: BytesMut,
raw: BytesMut,
Expand Down Expand Up @@ -87,7 +88,8 @@ impl VisionStream {
write_shutdown_queued: false,
write_buf: BytesMut::new(),
write_deframer: FuzzyTlsDeframer::new(),
write_filter: VisionFilter::new(),
read_deframer: FuzzyTlsDeframer::new(),
traffic_filter: VisionFilter::new(),
read_mode: ReadMode::Framed,
decoded: BytesMut::new(),
raw: BytesMut::new(),
Expand Down Expand Up @@ -118,24 +120,24 @@ impl VisionStream {
match self.write_deframer.next_record()? {
DeframeResult::TlsRecord(record) => {
processed_len += record.len();
self.write_filter.filter_record(&record);
self.traffic_filter.filter_record(&record);

let is_app_data = self.write_filter.is_tls()
let is_app_data = self.traffic_filter.is_tls()
&& record.len() >= 3
&& record[0] == TLS_APPLICATION_DATA
&& record[1] == 0x03;
let non_tls_filtering_ended = !is_app_data
&& !self.write_filter.is_filtering()
&& !self.write_filter.is_tls12_or_above();
&& !self.traffic_filter.is_filtering()
&& !self.traffic_filter.is_tls12_or_above();
let finish_padding_for_legacy_compat =
!self.write_filter.is_tls12_or_above()
&& self.write_filter.remaining_filter_count() <= 1;
!self.traffic_filter.is_tls12_or_above()
&& self.traffic_filter.remaining_filter_count() <= 1;

if is_app_data
|| non_tls_filtering_ended
|| finish_padding_for_legacy_compat
{
let command = if self.write_filter.supports_xtls() {
let command = if self.traffic_filter.supports_xtls() {
self.pending_write_switch = PendingWriteSwitch::Raw;
CMD_PADDING_DIRECT
} else {
Expand All @@ -150,21 +152,21 @@ impl VisionStream {
self.pad_frame(
&record,
CMD_PADDING_CONTINUE,
self.write_filter.is_tls(),
self.traffic_filter.is_tls(),
);
}
DeframeResult::UnknownPrefix(prefix) => {
processed_len += prefix.len();
self.write_filter.decrement_filter_count();
self.traffic_filter.decrement_filter_count();

if !self.write_filter.is_tls()
|| self.write_filter.remaining_filter_count() <= 1
if !self.traffic_filter.is_tls()
|| self.traffic_filter.remaining_filter_count() <= 1
{
self.pending_write_switch = PendingWriteSwitch::Tls;
self.pad_frame(
&prefix,
CMD_PADDING_END,
self.write_filter.is_tls(),
self.traffic_filter.is_tls(),
);
self.write_deframer.clear();
return Ok(processed_len.saturating_sub(existing_inner_len));
Expand All @@ -173,7 +175,7 @@ impl VisionStream {
self.pad_frame(
&prefix,
CMD_PADDING_CONTINUE,
self.write_filter.is_tls(),
self.traffic_filter.is_tls(),
);
}
DeframeResult::NeedData => break,
Expand Down Expand Up @@ -220,7 +222,7 @@ impl VisionStream {

let remaining = self.write_deframer.remaining_data().to_vec();
self.write_deframer.clear();
self.pad_frame(&remaining, CMD_PADDING_END, self.write_filter.is_tls());
self.pad_frame(&remaining, CMD_PADDING_END, self.traffic_filter.is_tls());
self.write_shutdown_queued = true;
self.pending_write_switch = PendingWriteSwitch::Tls;
}
Expand All @@ -234,6 +236,7 @@ impl VisionStream {
let changed = !result.content.is_empty() || result.command.is_some();

self.raw.clear();
self.observe_read_content(&result.content)?;
self.decoded.extend_from_slice(&result.content);

match result.command {
Expand All @@ -253,6 +256,29 @@ impl VisionStream {

Ok(changed)
}

/// Feed downlink TLS records into the same traffic filter used by the
/// uplink writer. xray-core shares one TrafficState between VisionReader
/// and VisionWriter; without this, the client never learns the negotiated
/// TLS version/cipher from ServerHello and cannot select Direct upstream.
fn observe_read_content(&mut self, content: &[u8]) -> io::Result<()> {
if content.is_empty() || !self.traffic_filter.is_filtering() {
return Ok(());
}

self.read_deframer.feed(content);
loop {
match self.read_deframer.next_record()? {
DeframeResult::TlsRecord(record) => {
self.traffic_filter.filter_record(&record);
}
DeframeResult::UnknownPrefix(_) => {
self.traffic_filter.decrement_filter_count();
}
DeframeResult::NeedData => return Ok(()),
}
}
}
}

impl AsyncRead for VisionStream {
Expand Down Expand Up @@ -362,14 +388,14 @@ impl AsyncWrite for VisionStream {
error!(
"VISION WRITE: TLS deframing failed, ending Vision framing: {err}"
);
this.write_filter.stop_filtering("write invalid TLS data");
this.traffic_filter.stop_filtering("write invalid TLS data");
let remaining = this.write_deframer.remaining_data().to_vec();
this.write_deframer.clear();
this.pending_write_switch = PendingWriteSwitch::Tls;
this.pad_frame(
&remaining,
CMD_PADDING_END,
this.write_filter.is_tls(),
this.traffic_filter.is_tls(),
);
buf.len()
}
Expand Down Expand Up @@ -541,6 +567,41 @@ mod tests {
assert_eq!(&buf[..n], raw_payload);
}

#[tokio::test]
async fn test_downlink_server_hello_enables_uplink_direct() {
let (mut vs, mut server) = make_vision_pair();
let client_hello = tls_record(0x16, Some(0x01), &[0; 32]);
let server_hello = tls13_server_hello();
let app_data = tls_record(TLS_APPLICATION_DATA, None, b"request");

vs.write_all(&client_hello).await.unwrap();
vs.flush().await.unwrap();

let mut uplink = vec![0u8; 65536];
let _ = server.read(&mut uplink).await.unwrap();

let mut downlink = vec![0, 0];
downlink.extend(server_first_frame(
&TEST_UUID,
CMD_PADDING_CONTINUE,
&server_hello,
0,
));
server.write_all(&downlink).await.unwrap();

let mut observed_server_hello = vec![0u8; server_hello.len()];
vs.read_exact(&mut observed_server_hello).await.unwrap();
assert_eq!(observed_server_hello, server_hello);

vs.write_all(&app_data).await.unwrap();
vs.flush().await.unwrap();

let n = server.read(&mut uplink).await.unwrap();
let (cmd, content, _, _) = parse_frame(&uplink[..n], 0);
assert_eq!(cmd, CMD_PADDING_DIRECT);
assert_eq!(content, app_data);
}

#[tokio::test]
async fn test_read_switches_to_direct_on_cmd_direct() {
let (mut vs, mut server) = make_vision_pair();
Expand Down Expand Up @@ -602,6 +663,28 @@ mod tests {
assert_eq!(vs.read_mode, ReadMode::Framed);
}

#[test]
fn test_fragmented_downlink_server_hello_enables_direct() {
let (mut vs, _) = make_vision_pair();
let server_hello = tls13_server_hello();
let split = server_hello.len() / 2;

vs.raw.extend(server_first_frame(
&TEST_UUID,
CMD_PADDING_CONTINUE,
&server_hello[..split],
0,
));
assert!(vs.process_raw_read().unwrap());
assert!(!vs.traffic_filter.supports_xtls());
vs.decoded.clear();

vs.raw
.extend(server_frame(CMD_PADDING_CONTINUE, &server_hello[split..]));
assert!(vs.process_raw_read().unwrap());
assert!(vs.traffic_filter.supports_xtls());
}

#[tokio::test]
async fn test_shutdown_sends_padding_end_frame() {
let (mut vs, mut server) = make_vision_pair();
Expand Down
76 changes: 72 additions & 4 deletions clash-lib/src/proxy/vless/vision_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use std::io;
const TLS_CONTENT_TYPE_HANDSHAKE: u8 = 0x16;
const TLS_HANDSHAKE_TYPE_CLIENT_HELLO: u8 = 0x01;
const TLS_HANDSHAKE_TYPE_SERVER_HELLO: u8 = 0x02;
const TLS13_CIPHER_AES_128_CCM_8_SHA256: u16 = 0x1305;
const TLS13_CIPHER_AES_128_GCM_SHA256: u16 = 0x1301;
const TLS13_CIPHER_AES_256_GCM_SHA384: u16 = 0x1302;
const TLS13_CIPHER_CHACHA20_POLY1305_SHA256: u16 = 0x1303;
const TLS13_CIPHER_AES_128_CCM_SHA256: u16 = 0x1304;

#[derive(Debug)]
pub struct VisionFilter {
Expand Down Expand Up @@ -88,9 +91,7 @@ impl VisionFilter {

match parse_server_hello(data) {
Ok(parsed) => {
if parsed.is_tls13
&& parsed.cipher_suite != TLS13_CIPHER_AES_128_CCM_8_SHA256
{
if parsed.is_tls13 && supports_xtls_cipher(parsed.cipher_suite) {
self.supports_xtls = true;
}
if parsed.is_tls13 {
Expand All @@ -105,6 +106,16 @@ impl VisionFilter {
}
}

fn supports_xtls_cipher(cipher_suite: u16) -> bool {
matches!(
cipher_suite,
TLS13_CIPHER_AES_128_GCM_SHA256
| TLS13_CIPHER_AES_256_GCM_SHA384
| TLS13_CIPHER_CHACHA20_POLY1305_SHA256
| TLS13_CIPHER_AES_128_CCM_SHA256
)
}

fn parse_server_hello(record: &[u8]) -> io::Result<ParsedServerHello> {
if record.len() < 5 + 4 + 2 + 32 + 1 {
return Err(io::Error::new(
Expand Down Expand Up @@ -190,3 +201,60 @@ fn parse_server_hello(record: &[u8]) -> io::Result<ParsedServerHello> {
is_tls13,
})
}

#[cfg(test)]
mod tests {
use super::VisionFilter;

fn server_hello(cipher: u16, tls13: bool) -> Vec<u8> {
let mut message = Vec::new();
message.extend_from_slice(&[0x03, 0x03]);
message.extend_from_slice(&[0x11; 32]);
message.push(0);
message.extend_from_slice(&cipher.to_be_bytes());
message.push(0);
if tls13 {
message.extend_from_slice(&6u16.to_be_bytes());
message.extend_from_slice(&[0x00, 0x2b, 0x00, 0x02, 0x03, 0x04]);
} else {
message.extend_from_slice(&0u16.to_be_bytes());
}

let mut handshake = vec![0x02];
handshake.extend_from_slice(&(message.len() as u32).to_be_bytes()[1..]);
handshake.extend_from_slice(&message);

let mut record = vec![0x16, 0x03, 0x03];
record.extend_from_slice(&(handshake.len() as u16).to_be_bytes());
record.extend_from_slice(&handshake);
record
}

#[test]
fn xray_tls13_cipher_matrix() {
for cipher in [0x1301, 0x1302, 0x1303, 0x1304] {
let mut filter = VisionFilter::new();
filter.filter_record(&server_hello(cipher, true));
assert!(filter.supports_xtls(), "cipher {cipher:#06x}");
}

for cipher in [0x1305, 0x0a0a, 0xc02f] {
let mut filter = VisionFilter::new();
filter.filter_record(&server_hello(cipher, true));
assert!(!filter.supports_xtls(), "cipher {cipher:#06x}");
}
}

#[test]
fn tls12_and_non_tls_never_enable_direct() {
let mut tls12 = VisionFilter::new();
tls12.filter_record(&server_hello(0xc02f, false));
assert!(tls12.is_tls12_or_above());
assert!(!tls12.supports_xtls());

let mut plaintext = VisionFilter::new();
plaintext.filter_record(b"GET / HTTP/1.1\r\n");
assert!(!plaintext.is_tls());
assert!(!plaintext.supports_xtls());
}
}
13 changes: 7 additions & 6 deletions config_reality_min.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
socks-port: 17890
bind-address: '*'
bind-address: 127.0.0.1
allow-lan: false
mode: rule
log-level: debug
ipv6: false
mmdb: null
dns:
enable: false

Expand All @@ -12,14 +13,14 @@ proxies:
type: vless
server: 127.0.0.1
port: 39444
uuid: 114cb5a6-3787-4357-a5da-69b5782cb74f
uuid: 3ac9b383-75a1-431c-8184-106c80eb2273
network: tcp
tls: true
servername:
client-fingerprint:
servername: www.apple.com
client-fingerprint: chrome
reality-opts:
public-key:
short-id:
public-key: lpaMu0U01fKbRO9mgkSiOArWZz4V0TRW7pR543Pm9Xg
short-id: 4ac97aaf8b9b0356
skip-cert-verify: true

rules:
Expand Down
Loading