diff --git a/clash-lib/src/proxy/transport/reality/reality_client_connection.rs b/clash-lib/src/proxy/transport/reality/reality_client_connection.rs index 3a92550c..a1f98f24 100644 --- a/clash-lib/src/proxy/transport/reality/reality_client_connection.rs +++ b/clash-lib/src/proxy/transport/reality/reality_client_connection.rs @@ -596,7 +596,10 @@ 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, @@ -604,7 +607,17 @@ impl RealityClientConnection { 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)", diff --git a/clash-lib/src/proxy/vless/vision.rs b/clash-lib/src/proxy/vless/vision.rs index 5e5eba88..8f690741 100644 --- a/clash-lib/src/proxy/vless/vision.rs +++ b/clash-lib/src/proxy/vless/vision.rs @@ -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, @@ -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(), @@ -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 { @@ -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)); @@ -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, @@ -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; } @@ -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 { @@ -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 { @@ -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() } @@ -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(); @@ -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(); diff --git a/clash-lib/src/proxy/vless/vision_filter.rs b/clash-lib/src/proxy/vless/vision_filter.rs index 463c3104..2eff5fe2 100644 --- a/clash-lib/src/proxy/vless/vision_filter.rs +++ b/clash-lib/src/proxy/vless/vision_filter.rs @@ -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 { @@ -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 { @@ -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 { if record.len() < 5 + 4 + 2 + 32 + 1 { return Err(io::Error::new( @@ -190,3 +201,60 @@ fn parse_server_hello(record: &[u8]) -> io::Result { is_tls13, }) } + +#[cfg(test)] +mod tests { + use super::VisionFilter; + + fn server_hello(cipher: u16, tls13: bool) -> Vec { + 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()); + } +} diff --git a/config_reality_min.yaml b/config_reality_min.yaml index 04b29dcd..85df5911 100644 --- a/config_reality_min.yaml +++ b/config_reality_min.yaml @@ -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 @@ -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: